diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 2f131d8..5a6c3ca 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -11,6 +11,7 @@ #include "hooks/audio/audio.h" #include "hooks/audio/mme.h" #include "hooks/graphics/graphics.h" +#include "misc/wintouchemu.h" #include "overlay/overlay.h" #include "touch/native/nativetouchhook.h" #include "util/cpuutils.h" @@ -653,7 +654,13 @@ namespace games::gitadora { // monitor/touch hooks (windowed or full screen) if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { // enable touch hook for subscreen overlay - nativetouch::hook(avs::game::DLL_INSTANCE); + const auto native_touch_ready = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!native_touch_ready) { + wintouchemu::FORCE = true; + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); + } #if !SPICE_XP diff --git a/src/spice2x/games/iidx/iidx.cpp b/src/spice2x/games/iidx/iidx.cpp index 75ec3ac..0154d37 100644 --- a/src/spice2x/games/iidx/iidx.cpp +++ b/src/spice2x/games/iidx/iidx.cpp @@ -22,6 +22,7 @@ #include "touch/touch.h" #include "touch/native/nativetouchhook.h" #include "misc/eamuse.h" +#include "misc/wintouchemu.h" #include "util/detour.h" #include "util/deferlog.h" #include "util/fileutils.h" @@ -40,6 +41,7 @@ #include "bi2x_hook.h" #include "ezusb.h" #include "io.h" +#include "poke.h" static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; @@ -62,6 +64,8 @@ namespace games::iidx { bool TDJ_MODE = false; bool FORCE_720P = false; bool DISABLE_ESPEC_IO = false; + bool NATIVE_TOUCH = true; + bool ENABLE_POKE = false; std::optional SOUND_OUTPUT_DEVICE = std::nullopt; std::optional SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt; std::optional ASIO_DRIVER = std::nullopt; @@ -348,7 +352,16 @@ namespace games::iidx { // need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook devicehook_init(avs::core::DLL_INSTANCE); - nativetouch::hook(avs::game::DLL_INSTANCE); + NATIVE_TOUCH = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!NATIVE_TOUCH) { + wintouchemu::FORCE = true; + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook_title_ends( + "beatmania IIDX", + "main", + avs::game::DLL_INSTANCE); + } } // insert BI2X hooks @@ -525,6 +538,12 @@ namespace games::iidx { } } + void IIDXGame::post_attach() { + if (ENABLE_POKE) { + poke::enable(); + } + } + void IIDXGame::detach() { Game::detach(); diff --git a/src/spice2x/games/iidx/iidx.h b/src/spice2x/games/iidx/iidx.h index cb27146..2855abc 100644 --- a/src/spice2x/games/iidx/iidx.h +++ b/src/spice2x/games/iidx/iidx.h @@ -28,6 +28,8 @@ namespace games::iidx { extern bool TDJ_MODE; extern bool FORCE_720P; extern bool DISABLE_ESPEC_IO; + extern bool NATIVE_TOUCH; + extern bool ENABLE_POKE; extern std::optional SOUND_OUTPUT_DEVICE; extern std::optional ASIO_DRIVER; extern uint8_t DIGITAL_TT_SENS; @@ -52,6 +54,7 @@ namespace games::iidx { virtual void attach() override; virtual void pre_attach() override; + virtual void post_attach() override; virtual void detach() override; private: diff --git a/src/spice2x/games/iidx/poke.cpp b/src/spice2x/games/iidx/poke.cpp index e270120..62abc0c 100644 --- a/src/spice2x/games/iidx/poke.cpp +++ b/src/spice2x/games/iidx/poke.cpp @@ -4,6 +4,7 @@ #include "windows.h" #include "games/io.h" +#include "games/iidx/iidx.h" #include "hooks/graphics/graphics.h" #include "launcher/shutdown.h" #include "misc/eamuse.h" @@ -116,6 +117,11 @@ namespace games::iidx::poke { if (THREAD) return; + if (!games::iidx::NATIVE_TOUCH) { + log_warning("poke", "keypad touch emulation requires native touch; disabled"); + return; + } + // create new thread THREAD_RUNNING = true; THREAD = new std::thread([] { diff --git a/src/spice2x/games/nost/nost.cpp b/src/spice2x/games/nost/nost.cpp index 4bb582d..1ea4726 100644 --- a/src/spice2x/games/nost/nost.cpp +++ b/src/spice2x/games/nost/nost.cpp @@ -2,6 +2,7 @@ #include "poke.h" #include "touch_mode.h" #include "hooks/setupapihook.h" +#include "misc/wintouchemu.h" #include "touch/native/nativetouchhook.h" #include "avs/game.h" @@ -41,9 +42,15 @@ namespace games::nost { setupapihook_init(avs::game::DLL_INSTANCE); setupapihook_add(touch_settings); - nativetouch::hook(avs::game::DLL_INSTANCE); - if (ENABLE_TOUCH_MODE) { - touch_mode::enable(); + const auto native_touch_ready = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!native_touch_ready) { + wintouchemu::FORCE = true; + wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20); + } else { + if (ENABLE_TOUCH_MODE) { + touch_mode::enable(); + } } } diff --git a/src/spice2x/games/nost/poke.cpp b/src/spice2x/games/nost/poke.cpp index 93e1794..49db8d7 100644 --- a/src/spice2x/games/nost/poke.cpp +++ b/src/spice2x/games/nost/poke.cpp @@ -9,6 +9,7 @@ #include "rawinput/rawinput.h" #include "misc/eamuse.h" #include "touch/native/inject.h" +#include "touch/native/nativetouchhook.h" #include "util/logging.h" #include "util/precise_timer.h" @@ -59,6 +60,11 @@ namespace games::nost::poke { return; } + if (!nativetouch::is_hooked()) { + log_warning("poke", "Nostalgia poke requires native touch; disabled"); + return; + } + // create new thread THREAD_RUNNING = true; THREAD = new std::thread([] { diff --git a/src/spice2x/games/popn/popn.cpp b/src/spice2x/games/popn/popn.cpp index a6b6744..3853651 100644 --- a/src/spice2x/games/popn/popn.cpp +++ b/src/spice2x/games/popn/popn.cpp @@ -16,6 +16,7 @@ #include "launcher/launcher.h" #include "launcher/logger.h" #include "misc/eamuse.h" +#include "misc/wintouchemu.h" #include "util/sysutils.h" #include "io.h" #include "util/deferlog.h" @@ -24,6 +25,7 @@ namespace games::popn { bool SHOW_PIKA_MONITOR_WARNING = false; + bool NATIVE_TOUCH = true; #if SPICE64 && !SPICE_XP @@ -713,7 +715,18 @@ namespace games::popn { // 00000100 0000000B 00000001 (button 9) // set third column to 0 and it will work with BIO2 - nativetouch::hook(avs::game::DLL_INSTANCE); + NATIVE_TOUCH = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!NATIVE_TOUCH) { + wintouchemu::FORCE = true; + if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) { + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook_title_ends( + "", + "Main Screen", + avs::game::DLL_INSTANCE); + } + } sysutils::hook_EnumDisplayDevicesA(); diff --git a/src/spice2x/games/popn/popn.h b/src/spice2x/games/popn/popn.h index 06e4166..105e3b9 100644 --- a/src/spice2x/games/popn/popn.h +++ b/src/spice2x/games/popn/popn.h @@ -22,6 +22,7 @@ namespace games::popn { #endif extern bool SHOW_PIKA_MONITOR_WARNING; + extern bool NATIVE_TOUCH; class POPNGame : public games::Game { public: diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index 80595ba..a263e4a 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -26,6 +26,7 @@ #include "util/libutils.h" #include "util/sysutils.h" #include "misc/eamuse.h" +#include "misc/wintouchemu.h" #include "touch/native/nativetouchhook.h" #include "bi2x_hook.h" #include "camera.h" @@ -453,7 +454,18 @@ namespace games::sdvx { } if (is_valkyrie_model()) { - nativetouch::hook(avs::game::DLL_INSTANCE); + const auto native_touch_ready = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!native_touch_ready) { + wintouchemu::FORCE = true; + if (!GRAPHICS_WINDOWED) { + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook_title_ends( + "SOUND VOLTEX", + "Main Screen", + avs::game::DLL_INSTANCE); + } + } // insert BI2X hooks bi2x_hook_init(); diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 1e324e3..9e28607 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -28,6 +28,7 @@ #include "util/logging.h" #include "util/fileutils.h" #include "util/utils.h" +#include "misc/wintouchemu.h" #include "touch/native/inject.h" #include "util/time.h" #include "rawinput/rawinput.h" @@ -307,6 +308,22 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l } } + if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) { + // drop mouse inputs since only wintouches should be used + switch (uMsg) { + case WM_MOUSEMOVE: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + return true; + } + } + // window resize graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam); @@ -786,7 +803,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei nWidth = rect.right - rect.left; nHeight = rect.bottom - rect.top; - nativetouch::inject::register_and_attach_window(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 { // Existing behaviour: suppress subscreen window and prompt user to use overlay instead log_info( @@ -1160,8 +1181,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast(WindowProc)); const bool native_touch_overlay = - (games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || - (games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); + (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || + (games::popn::NATIVE_TOUCH && + games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); if (native_touch_overlay) { nativetouch::inject::register_and_attach_window(hWnd); } diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 160acb0..385a540 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -38,10 +38,10 @@ #include "games/gitadora/gitadora.h" #include "games/hpm/hpm.h" #include "games/iidx/iidx.h" +#include "games/iidx/poke.h" #ifdef SPICE64 #include "games/iidx/camera.h" #endif -#include "games/iidx/poke.h" #include "games/jb/jb.h" #include "games/mga/mga.h" #include "games/nost/nost.h" @@ -1783,7 +1783,7 @@ int main_implementation(int argc, char *argv[]) { // enable subscreen touch emulation if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) { - games::iidx::poke::enable(); + games::iidx::ENABLE_POKE = true; } if (options[launcher::Options::NostalgiaPoke].is_active()) { games::nost::ENABLE_POKE = true; diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index cf56864..d451c26 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1791,10 +1791,10 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Touch Parameters", }, { - .title = "Force Touch Emulation", + .title = "Force Legacy Touch Emulation", .name = "touchemuforce", .desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. " - "This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.", + "Do not enable this unless you have trouble with using a mouse to emulate touch input.", .type = OptionType::Bool, .category = "Touch Parameters", }, diff --git a/src/spice2x/misc/wintouchemu.cpp b/src/spice2x/misc/wintouchemu.cpp index bdb5d13..8b1eb42 100644 --- a/src/spice2x/misc/wintouchemu.cpp +++ b/src/spice2x/misc/wintouchemu.cpp @@ -4,18 +4,43 @@ #include "wintouchemu.h" +#include +#include +#include +#include + +#include "cfg/screen_resize.h" +#include "games/gitadora/gitadora.h" +#include "games/iidx/iidx.h" +#include "games/popn/popn.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 "rawinput/touch.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; 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; @@ -56,13 +81,16 @@ namespace wintouchemu { // state BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int); + bool USE_MOUSE = false; std::vector TOUCH_EVENTS; std::vector TOUCH_POINTS; HMODULE HOOKED_MODULE = nullptr; std::string WINDOW_TITLE_START = ""; - bool INITIALIZED = false; + std::optional WINDOW_TITLE_END = std::nullopt; + volatile bool INITIALIZED = false; + mouse_state_t mouse_state; - void hook(const char *window_title, HMODULE module) { + void hook(const char *window_title, HMODULE module, int delay_in_s) { // hooks auto system_metrics_hook = detour::iat_try( @@ -81,8 +109,31 @@ namespace wintouchemu { // set module and title HOOKED_MODULE = module; WINDOW_TITLE_START = window_title; - log_misc("wintouchemu", "initializing"); - INITIALIZED = true; + + 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; + } + } + + 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 void flip_touch_points(PTOUCHINPUT point) { + point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x; + point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y; } static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { @@ -94,6 +145,7 @@ namespace wintouchemu { // set touch inputs bool result = false; + bool mouse_used = false; for (UINT input = 0; input < cInputs; input++) { auto *touch_input = &pInputs[input]; @@ -126,7 +178,13 @@ namespace wintouchemu { // log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y)); - if (overlay::OVERLAY) { + 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 { @@ -172,10 +230,89 @@ namespace wintouchemu { touch_input->cxContact = 0; touch_input->cyContact = 0; - } else { + if (avs::game::is_model("KFC") && + rawinput::touch::DISPLAY_INITIALIZED && + rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) { + flip_touch_points(touch_input); + } + + } 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 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. + */ - // beatstream requires a MOVE for active points in each update - // add one for every active point without a matching input event // find touch point which has no associated input event TouchPoint *touch_point = nullptr; for (auto &tp : TOUCH_POINTS) { @@ -243,6 +380,21 @@ namespace wintouchemu { 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; @@ -250,13 +402,39 @@ namespace wintouchemu { // check if windowed if (GRAPHICS_WINDOWED) { - // 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); + 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 ldj overlay subscreen"); + USE_MOUSE = true; + } else if (games::popn::is_pikapika_model()) { + // same as iidx case above + log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen"); + USE_MOUSE = true; + } else 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 @@ -267,6 +445,8 @@ namespace wintouchemu { } } + const auto now = get_performance_milliseconds(); + // update touch events if (hWnd != nullptr) { @@ -286,11 +466,56 @@ namespace wintouchemu { // 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); + } } } } diff --git a/src/spice2x/misc/wintouchemu.h b/src/spice2x/misc/wintouchemu.h index ef57f38..c1e6eb4 100644 --- a/src/spice2x/misc/wintouchemu.h +++ b/src/spice2x/misc/wintouchemu.h @@ -6,8 +6,14 @@ namespace wintouchemu { // settings extern bool FORCE; + extern bool INJECT_MOUSE_AS_WM_TOUCH; + extern bool LOG_FPS; extern bool ADD_TOUCH_FLAG_PRIMARY; - void hook(const char *window_title, HMODULE module = nullptr); + void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0); + void hook_title_ends( + const char *window_title_start, + const char *window_title_ends, + HMODULE module = nullptr); void update(); } diff --git a/src/spice2x/touch/native/inject.cpp b/src/spice2x/touch/native/inject.cpp index 64bd6a7..e009152 100644 --- a/src/spice2x/touch/native/inject.cpp +++ b/src/spice2x/touch/native/inject.cpp @@ -338,9 +338,7 @@ namespace nativetouch::inject { // 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 (!initialize_touch_injection()) { if (register_touch) { log_warning( "touch::native", @@ -422,7 +420,7 @@ namespace nativetouch::inject { } // load and initialize Windows 8 touch injection without a static API dependency - void initialize_touch_injection() { + bool initialize_touch_injection() { std::call_once(initialization_once, [] { initialize_synthetic_touch(); contact_refresh_message = @@ -433,17 +431,26 @@ namespace nativetouch::inject { } // load all APIs from user32 without adding static imports + // + // note that these are expected to be present in Windows 8 and above; + // however, on WINE, touch implementation remains in Windows 7 era + // and therefore the hooks below will fail, hence the fallback to + // legacy wintouchemu code const auto user32 = libutils::load_library("user32.dll"); InitializeTouchInjection_ptr = libutils::try_proc( user32, "InitializeTouchInjection"); + if (InitializeTouchInjection_ptr == nullptr) { + log_warning( + "touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled"); + return; + } + InjectTouchInput_ptr = libutils::try_proc( user32, "InjectTouchInput"); - - if (InitializeTouchInjection_ptr == nullptr || - InjectTouchInput_ptr == nullptr) { + if (InjectTouchInput_ptr == nullptr) { clear_touch_injection_functions(); log_warning( - "touch::native", "touch injection API unavailable; mouse touch injection disabled"); + "touch::native", "InjectTouchInput unavailable; mouse touch injection disabled"); return; } @@ -457,20 +464,31 @@ namespace nativetouch::inject { log_misc("touch::native", "mouse touch injection initialized"); }); - } - - bool touch_injection_available() { - return InjectTouchInput_ptr != nullptr; + return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr; } // install injection support for touch windows registered by the game module - void hook(HMODULE module) { - initialize_touch_injection(); + bool hook_available(HMODULE module) { + if (detour::iat_find("RegisterTouchWindow", module) == nullptr) { + log_warning("touch::native", "RegisterTouchWindow unavailable"); + return false; + } + return true; + } + + bool hook(HMODULE module) { + if (!initialize_touch_injection()) { + return false; + } RegisterTouchWindow_orig = detour::iat_try( "RegisterTouchWindow", RegisterTouchWindowHook, module); - if (RegisterTouchWindow_orig != nullptr) { - log_misc("touch::native", "RegisterTouchWindow hooked"); + if (RegisterTouchWindow_orig == nullptr) { + log_warning("touch::native", "failed to hook RegisterTouchWindow"); + return false; } + + log_misc("touch::native", "RegisterTouchWindow hooked"); + return true; } } diff --git a/src/spice2x/touch/native/inject.h b/src/spice2x/touch/native/inject.h index cd45a66..d7077b3 100644 --- a/src/spice2x/touch/native/inject.h +++ b/src/spice2x/touch/native/inject.h @@ -7,7 +7,8 @@ struct tagTOUCHINPUT; namespace nativetouch::inject { void attach_window(HWND window); void register_and_attach_window(HWND window); - void hook(HMODULE module); + bool hook_available(HMODULE module); + bool hook(HMODULE module); bool inject_synthetic_touch(POINT position, bool down); bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down); bool transform_touch_input(tagTOUCHINPUT *point); diff --git a/src/spice2x/touch/native/inject_internal.h b/src/spice2x/touch/native/inject_internal.h index 838248e..53f217f 100644 --- a/src/spice2x/touch/native/inject_internal.h +++ b/src/spice2x/touch/native/inject_internal.h @@ -10,9 +10,8 @@ namespace nativetouch::inject { Synthetic, }; - void initialize_touch_injection(); + bool initialize_touch_injection(); void initialize_synthetic_touch(); - bool touch_injection_available(); void refresh_contact_lifetime(); bool contact_is_active(); diff --git a/src/spice2x/touch/native/inject_synthetic.cpp b/src/spice2x/touch/native/inject_synthetic.cpp index 83f1b32..41483bd 100644 --- a/src/spice2x/touch/native/inject_synthetic.cpp +++ b/src/spice2x/touch/native/inject_synthetic.cpp @@ -113,11 +113,12 @@ namespace nativetouch::inject { } static HWND prepare_synthetic_touch() { - initialize_touch_injection(); + if (!initialize_touch_injection()) { + return nullptr; + } const auto window = get_injection_window(); - if (!touch_injection_available() || window == nullptr || - synthetic_touch_message == 0) { + if (window == nullptr || synthetic_touch_message == 0) { return nullptr; } return window; diff --git a/src/spice2x/touch/native/nativetouchhook.cpp b/src/spice2x/touch/native/nativetouchhook.cpp index b86d190..cfada7d 100644 --- a/src/spice2x/touch/native/nativetouchhook.cpp +++ b/src/spice2x/touch/native/nativetouchhook.cpp @@ -223,25 +223,58 @@ namespace nativetouch { } } - void hook(HMODULE module) { + static bool hook_prerequisites_available(HMODULE module) { + if (detour::iat_find("GetTouchInputInfo", module) == nullptr) { + log_warning("touch::native", "GetTouchInputInfo unavailable"); + return false; + } + if (settings::EMULATE_DIGITIZER && + detour::iat_find("GetSystemMetrics", module) == nullptr) { + log_warning("touch::native", "GetSystemMetrics unavailable"); + return false; + } + return true; + } + + bool hook(HMODULE module) { + native_touch_hooked = false; initialize_game_settings(); - inject::hook(module); + // check if the OS supports touch API (Win7+) and injection API (requires Win8+) + if (!hook_prerequisites_available(module)) { + return false; + } + if (!inject::hook_available(module)) { + return false; + } - native_touch_hooked = true; + // try hooking injection API first since they require the highest OS level + // (WINE specifically did not implement this in 2026) + if (!inject::hook(module)) { + return false; + } + // GetSystemMetrics if (settings::EMULATE_DIGITIZER) { GetSystemMetrics_orig = detour::iat_try( "GetSystemMetrics", GetSystemMetricsHook, module); - if (GetSystemMetrics_orig != nullptr) { - log_misc("touch::native", "GetSystemMetrics hooked"); + if (GetSystemMetrics_orig == nullptr) { + log_warning("touch::native", "failed to hook GetSystemMetrics"); + return false; } + log_misc("touch::native", "GetSystemMetrics hooked"); } + // GetTouchInputInfo GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); - if (GetTouchInputInfo_orig != nullptr) { - log_misc("touch::native", "GetTouchInputInfo hooked"); + if (GetTouchInputInfo_orig == nullptr) { + log_warning("touch::native", "failed to hook GetTouchInputInfo"); + return false; } + log_misc("touch::native", "GetTouchInputInfo hooked"); + + native_touch_hooked = true; + return true; } } diff --git a/src/spice2x/touch/native/nativetouchhook.h b/src/spice2x/touch/native/nativetouchhook.h index e66045c..5598438 100644 --- a/src/spice2x/touch/native/nativetouchhook.h +++ b/src/spice2x/touch/native/nativetouchhook.h @@ -15,7 +15,7 @@ namespace nativetouch { using TouchInputFilter = bool (*)(const NativeTouchEvent &event); - void hook(HMODULE module); + bool hook(HMODULE module); void refresh_contact_lifetime(); void set_input_filter(TouchInputFilter filter);