## Link to GitHub Issue or related Pull Request, if one exists Fixes #814 ## Description of change Remove iidx/sdvx/popon `native touch` options. Remove dead code in wintouchemu that deals with these games. Native touch is now at feature-parity with wintouchemu. Native touch options work better with touch screens, while rawinput-based wintouchemu has compat issues on some touchscreens. For these subscreen games deprecate the usage of wintouchemu and make them use native touch stack by default. ### For future consideration Gitadora - not sure what to do with this one. We only use wintouchemu for the single window case. For all other configurations, the game accepts mouse and touch input without any hooks, so perhaps nothing needs to be done here. Nostalgia - consider moving away from wintouchemu and adopt native touch hook here as well. ## Testing WIP
461 lines
17 KiB
C++
461 lines
17 KiB
C++
// enable touch functions - set version to windows 7
|
|
// mingw otherwise doesn't load touch stuff
|
|
#define _WIN32_WINNT 0x0601
|
|
|
|
#include "wintouchemu.h"
|
|
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
#include <thread>
|
|
|
|
#include "games/gitadora/gitadora.h"
|
|
#include "hooks/graphics/graphics.h"
|
|
#include "overlay/overlay.h"
|
|
#include "touch/touch.h"
|
|
#include "util/detour.h"
|
|
#include "util/logging.h"
|
|
#include "util/time.h"
|
|
#include "util/utils.h"
|
|
|
|
namespace wintouchemu {
|
|
|
|
typedef struct {
|
|
POINT pos;
|
|
bool last_button_pressed;
|
|
DWORD touch_event;
|
|
} mouse_state_t;
|
|
|
|
// settings
|
|
bool FORCE = false;
|
|
bool INJECT_MOUSE_AS_WM_TOUCH = false;
|
|
bool LOG_FPS = false;
|
|
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
|
|
|
// state
|
|
double last_touch_event = 0.0;
|
|
|
|
static inline bool is_emu_enabled() {
|
|
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
|
}
|
|
|
|
static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr;
|
|
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
|
|
|
|
static int WINAPI GetSystemMetrics_hook(int nIndex) {
|
|
|
|
/*
|
|
* fake touch screen
|
|
* the game requires 0x01 and 0x02 flags to be set
|
|
* 0x40 and 0x80 are set for completeness
|
|
*/
|
|
if (nIndex == 94)
|
|
return 0x01 | 0x02 | 0x40 | 0x80;
|
|
|
|
// call original
|
|
if (GetSystemMetrics_orig != nullptr) {
|
|
return GetSystemMetrics_orig(nIndex);
|
|
}
|
|
|
|
// return error
|
|
return 0;
|
|
}
|
|
|
|
static BOOL WINAPI RegisterTouchWindow_hook(HWND hwnd, ULONG ulFlags) {
|
|
|
|
// don't register it if the emu is enabled
|
|
if (is_emu_enabled()) {
|
|
return true;
|
|
}
|
|
|
|
// call default
|
|
return RegisterTouchWindow_orig(hwnd, ulFlags);
|
|
}
|
|
|
|
// state
|
|
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
|
|
bool USE_MOUSE = false;
|
|
std::vector<TouchEvent> TOUCH_EVENTS;
|
|
std::vector<TouchPoint> TOUCH_POINTS;
|
|
HMODULE HOOKED_MODULE = nullptr;
|
|
std::string WINDOW_TITLE_START = "";
|
|
volatile bool INITIALIZED = false;
|
|
mouse_state_t mouse_state;
|
|
|
|
void hook(const char *window_title, HMODULE module, int delay_in_s) {
|
|
|
|
// hooks
|
|
auto system_metrics_hook = detour::iat_try(
|
|
"GetSystemMetrics", GetSystemMetrics_hook, module);
|
|
auto register_touch_window_hook = detour::iat_try(
|
|
"RegisterTouchWindow", RegisterTouchWindow_hook, module);
|
|
|
|
// don't hook twice
|
|
if (GetSystemMetrics_orig == nullptr) {
|
|
GetSystemMetrics_orig = system_metrics_hook;
|
|
}
|
|
if (RegisterTouchWindow_orig == nullptr) {
|
|
RegisterTouchWindow_orig = register_touch_window_hook;
|
|
}
|
|
|
|
// set module and title
|
|
HOOKED_MODULE = module;
|
|
WINDOW_TITLE_START = window_title;
|
|
|
|
if (0 < delay_in_s) {
|
|
// some games crash when touch events are injected too early during boot
|
|
std::thread t([&]() {
|
|
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
|
|
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
|
|
log_misc("wintouchemu", "initializing", delay_in_s);
|
|
INITIALIZED = true;
|
|
});
|
|
t.detach();
|
|
} else {
|
|
log_misc("wintouchemu", "initializing");
|
|
INITIALIZED = true;
|
|
}
|
|
}
|
|
|
|
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
|
|
|
// check if original should be called
|
|
if (hTouchInput != GetTouchInputInfoHook) {
|
|
return GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
|
}
|
|
|
|
// set touch inputs
|
|
bool result = false;
|
|
bool mouse_used = false;
|
|
for (UINT input = 0; input < cInputs; input++) {
|
|
auto *touch_input = &pInputs[input];
|
|
|
|
// clear touch input
|
|
touch_input->x = 0;
|
|
touch_input->y = 0;
|
|
touch_input->hSource = nullptr;
|
|
touch_input->dwID = 0;
|
|
touch_input->dwFlags = 0;
|
|
touch_input->dwMask = 0;
|
|
touch_input->dwTime = 0;
|
|
touch_input->dwExtraInfo = 0;
|
|
touch_input->cxContact = 0;
|
|
touch_input->cyContact = 0;
|
|
|
|
// get touch event
|
|
TouchEvent *touch_event = nullptr;
|
|
if (TOUCH_EVENTS.size() > input) {
|
|
touch_event = &TOUCH_EVENTS.at(input);
|
|
}
|
|
|
|
// check touch point
|
|
if (touch_event) {
|
|
// set touch point
|
|
result = true;
|
|
|
|
auto x = touch_event->x;
|
|
auto y = touch_event->y;
|
|
auto valid = true;
|
|
|
|
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
|
|
|
|
if (overlay::OVERLAY) {
|
|
// touch was received on global coords
|
|
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
|
|
} else {
|
|
valid = false;
|
|
}
|
|
|
|
touch_input->x = x * 100;
|
|
touch_input->y = y * 100;
|
|
touch_input->hSource = hTouchInput;
|
|
touch_input->dwID = touch_event->id;
|
|
touch_input->dwFlags = 0;
|
|
switch (touch_event->type) {
|
|
case TOUCH_DOWN:
|
|
if (valid) {
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
|
}
|
|
break;
|
|
case TOUCH_MOVE:
|
|
if (valid) {
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
|
}
|
|
break;
|
|
case TOUCH_UP:
|
|
// don't check valid so that this touch ID can be released
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_UP;
|
|
break;
|
|
}
|
|
touch_input->dwMask = 0;
|
|
touch_input->dwTime = 0;
|
|
touch_input->dwExtraInfo = 0;
|
|
touch_input->cxContact = 0;
|
|
touch_input->cyContact = 0;
|
|
|
|
} else if (USE_MOUSE && !mouse_used) {
|
|
|
|
// disable further mouse inputs this call
|
|
mouse_used = true;
|
|
|
|
if (mouse_state.touch_event) {
|
|
result = true;
|
|
touch_input->x = mouse_state.pos.x;
|
|
touch_input->y = mouse_state.pos.y;
|
|
|
|
if (GRAPHICS_WINDOWED) {
|
|
touch_input->x -= SPICETOUCH_TOUCH_X;
|
|
touch_input->y -= SPICETOUCH_TOUCH_Y;
|
|
}
|
|
|
|
// log_misc(
|
|
// "wintouchemu",
|
|
// "mouse state ({}, {}) event={}",
|
|
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
|
|
|
|
auto valid = true;
|
|
if (overlay::OVERLAY) {
|
|
valid = overlay::OVERLAY->transform_touch_point(
|
|
&touch_input->x, &touch_input->y);
|
|
}
|
|
|
|
// touch inputs require 100x precision per pixel
|
|
touch_input->x *= 100;
|
|
touch_input->y *= 100;
|
|
touch_input->hSource = hTouchInput;
|
|
touch_input->dwID = 0;
|
|
touch_input->dwFlags = 0;
|
|
switch (mouse_state.touch_event) {
|
|
case TOUCHEVENTF_DOWN:
|
|
if (valid) {
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
|
}
|
|
break;
|
|
case TOUCHEVENTF_MOVE:
|
|
if (valid) {
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
|
}
|
|
break;
|
|
case TOUCHEVENTF_UP:
|
|
// don't check valid so that this touch ID can be released
|
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
|
}
|
|
|
|
touch_input->dwFlags |= TOUCHEVENTF_UP;
|
|
break;
|
|
}
|
|
touch_input->dwMask = 0;
|
|
touch_input->dwTime = 0;
|
|
touch_input->dwExtraInfo = 0;
|
|
touch_input->cxContact = 0;
|
|
touch_input->cyContact = 0;
|
|
|
|
// reset it since the event was consumed & propagated as touch
|
|
mouse_state.touch_event = 0;
|
|
}
|
|
} else {
|
|
|
|
/*
|
|
* For some reason, Nostalgia won't show an active touch point unless a move event
|
|
* triggers in the same frame. To work around this, we just supply a fake move
|
|
* event if we didn't update the same pointer ID in the same call.
|
|
*/
|
|
|
|
// find touch point which has no associated input event
|
|
TouchPoint *touch_point = nullptr;
|
|
for (auto &tp : TOUCH_POINTS) {
|
|
bool found = false;
|
|
for (UINT i = 0; i < cInputs; i++) {
|
|
if (input > 0 && pInputs[i].dwID == tp.id) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
touch_point = &tp;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if unused touch point was found
|
|
if (touch_point) {
|
|
|
|
// set touch point
|
|
result = true;
|
|
touch_input->x = touch_point->x * 100;
|
|
touch_input->y = touch_point->y * 100;
|
|
touch_input->hSource = hTouchInput;
|
|
touch_input->dwID = touch_point->id;
|
|
touch_input->dwFlags = 0;
|
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
|
touch_input->dwMask = 0;
|
|
touch_input->dwTime = 0;
|
|
touch_input->dwExtraInfo = 0;
|
|
touch_input->cxContact = 0;
|
|
touch_input->cyContact = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// return success
|
|
return result;
|
|
}
|
|
|
|
void update() {
|
|
|
|
// check if initialized
|
|
if (!INITIALIZED) {
|
|
return;
|
|
}
|
|
|
|
// no need for hooks if touch is available
|
|
if (!is_emu_enabled()) {
|
|
return;
|
|
}
|
|
|
|
// get window handle
|
|
static HWND hWnd = nullptr;
|
|
if (hWnd == nullptr) {
|
|
|
|
// start with the active foreground window
|
|
hWnd = GetForegroundWindow();
|
|
auto title = get_window_title(hWnd);
|
|
|
|
// if the foreground window does not match the window title start, find a window
|
|
// that does
|
|
if (!string_begins_with(title, WINDOW_TITLE_START)) {
|
|
hWnd = FindWindowBeginsWith(WINDOW_TITLE_START);
|
|
title = get_window_title(hWnd);
|
|
}
|
|
|
|
// check window
|
|
if (hWnd == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// check if windowed
|
|
if (GRAPHICS_WINDOWED) {
|
|
if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
|
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
|
|
USE_MOUSE = true;
|
|
} else {
|
|
// create touch window - create overlay if not yet existing at this point
|
|
log_info("wintouchemu", "create touch window relative to main game window");
|
|
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
|
|
USE_MOUSE = false;
|
|
}
|
|
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
|
|
log_info(
|
|
"wintouchemu",
|
|
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
|
|
USE_MOUSE = true;
|
|
} else {
|
|
log_info("wintouchemu", "activating DirectX hooks");
|
|
// mouse position based input only
|
|
touch_attach_dx_hook();
|
|
USE_MOUSE = false;
|
|
}
|
|
|
|
// hooks
|
|
auto GetTouchInputInfo_orig_new = detour::iat_try(
|
|
"GetTouchInputInfo", GetTouchInputInfoHook, HOOKED_MODULE);
|
|
if (GetTouchInputInfo_orig == nullptr) {
|
|
GetTouchInputInfo_orig = GetTouchInputInfo_orig_new;
|
|
}
|
|
}
|
|
|
|
const auto now = get_performance_milliseconds();
|
|
|
|
// update touch events
|
|
if (hWnd != nullptr) {
|
|
|
|
// get touch events
|
|
TOUCH_EVENTS.clear();
|
|
touch_get_events(TOUCH_EVENTS);
|
|
|
|
// get touch points
|
|
TOUCH_POINTS.clear();
|
|
touch_get_points(TOUCH_POINTS);
|
|
|
|
// get event count
|
|
auto event_count = TOUCH_EVENTS.size();
|
|
|
|
// for the fake move events
|
|
event_count += MAX(0, (int) (TOUCH_POINTS.size() - TOUCH_EVENTS.size()));
|
|
|
|
// check if new events are available
|
|
if (event_count > 0) {
|
|
last_touch_event = now;
|
|
|
|
// send fake event to make the game update it's touch inputs
|
|
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
|
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
|
|
}
|
|
|
|
// update frame logging
|
|
if (LOG_FPS) {
|
|
static int log_frames = 0;
|
|
static uint64_t log_time = 0;
|
|
log_frames++;
|
|
if (log_time < get_system_seconds()) {
|
|
if (log_time > 0) {
|
|
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
|
|
}
|
|
log_frames = 0;
|
|
log_time = get_system_seconds();
|
|
}
|
|
}
|
|
}
|
|
|
|
// send separate WM_TOUCH event for mouse
|
|
// this must be separate from actual touch events because some games will ignore the return
|
|
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
|
|
// send empty events when the mouse button is not clicked/released
|
|
if (hWnd != nullptr && USE_MOUSE) {
|
|
bool button_pressed = get_async_primary_mouse();
|
|
|
|
// if there was a touch event in the last 500 ms, don't insert new button presses
|
|
if (button_pressed && (now - last_touch_event) < 500) {
|
|
button_pressed = false;
|
|
}
|
|
|
|
// figure out what kind of touch event to simulate
|
|
if (button_pressed && !mouse_state.last_button_pressed) {
|
|
mouse_state.touch_event = TOUCHEVENTF_DOWN;
|
|
} else if (button_pressed && mouse_state.last_button_pressed) {
|
|
mouse_state.touch_event = TOUCHEVENTF_MOVE;
|
|
} else if (!button_pressed && mouse_state.last_button_pressed) {
|
|
mouse_state.touch_event = TOUCHEVENTF_UP;
|
|
}
|
|
|
|
mouse_state.last_button_pressed = button_pressed;
|
|
if (mouse_state.touch_event) {
|
|
GetCursorPos(&mouse_state.pos);
|
|
// send fake event to make the game update it's touch inputs
|
|
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
|
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|