447 lines
16 KiB
C++
447 lines
16 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 <algorithm>
|
||
|
#include <optional>
|
||
|
|
||
|
#include "cfg/screen_resize.h"
|
||
|
#include "games/iidx/iidx.h"
|
||
|
#include "hooks/graphics/graphics.h"
|
||
|
#include "overlay/overlay.h"
|
||
|
#include "overlay/windows/generic_sub.h"
|
||
|
#include "touch/touch.h"
|
||
|
#include "util/detour.h"
|
||
|
#include "util/logging.h"
|
||
|
#include "util/time.h"
|
||
|
#include "util/utils.h"
|
||
|
|
||
|
#include "avs/game.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;
|
||
|
|
||
|
static inline bool is_emu_enabled() {
|
||
|
return FORCE || !is_touch_available() || 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 = "";
|
||
|
std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
|
||
|
bool INITIALIZED = false;
|
||
|
mouse_state_t mouse_state;
|
||
|
|
||
|
void hook(const char *window_title, HMODULE module) {
|
||
|
|
||
|
// 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;
|
||
|
INITIALIZED = true;
|
||
|
}
|
||
|
|
||
|
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
|
||
|
hook(window_title_start, module);
|
||
|
|
||
|
WINDOW_TITLE_END = window_title_end;
|
||
|
}
|
||
|
|
||
|
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 (GRAPHICS_IIDX_WSUB) {
|
||
|
// touch was received on subscreen window.
|
||
|
RECT clientRect {};
|
||
|
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
|
||
|
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
|
||
|
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
|
||
|
} else 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) {
|
||
|
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
||
|
}
|
||
|
break;
|
||
|
case TOUCH_MOVE:
|
||
|
if (valid) {
|
||
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
||
|
}
|
||
|
break;
|
||
|
case TOUCH_UP:
|
||
|
// don't check valid so that this touch ID can be released
|
||
|
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 ({}, {})", to_string(touch_input->x), to_string(touch_input->y));
|
||
|
|
||
|
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) {
|
||
|
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
||
|
}
|
||
|
break;
|
||
|
case TOUCHEVENTF_MOVE:
|
||
|
if (valid) {
|
||
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
||
|
}
|
||
|
break;
|
||
|
case TOUCHEVENTF_UP:
|
||
|
// don't check valid so that this touch ID can be released
|
||
|
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 if (!GRAPHICS_IIDX_WSUB) {
|
||
|
|
||
|
/*
|
||
|
* 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);
|
||
|
}
|
||
|
|
||
|
// if a window title end is set, check to see if it matches
|
||
|
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||
|
hWnd = nullptr;
|
||
|
title = "";
|
||
|
|
||
|
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
|
||
|
auto check_title = get_window_title(window);
|
||
|
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||
|
hWnd = std::move(window);
|
||
|
title = std::move(check_title);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check window
|
||
|
if (hWnd == nullptr) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// check if windowed
|
||
|
if (GRAPHICS_WINDOWED) {
|
||
|
if (GRAPHICS_IIDX_WSUB) {
|
||
|
// no handling is needed here
|
||
|
// graphics::MoveWindow_hook will attach hook to windowed subscreen
|
||
|
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
|
||
|
USE_MOUSE = false;
|
||
|
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
|
||
|
// overlay subscreen in IIDX
|
||
|
// use mouse position as ImGui overlay will block the touch window
|
||
|
log_info("wintouchemu", "use mouse cursor API for 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
|
||
|
// 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 = ((GetKeyState(VK_LBUTTON) & 0x100) != 0);
|
||
|
|
||
|
// 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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|