From 09ed8b948cb512f28df21dd8c1a7feae1cff185a Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:56:03 -0700 Subject: [PATCH] nost: move nostalgia from wintouchemu to native touch hook (#827) ## Link to GitHub Issue or related Pull Request, if one exists #814 ## Description of change Switch over Nostalgia from wintouchemu to native touch hook. Nostalgia has some strict timing requirements (touches must be updated on every acio poll) so this takes a slightly different path to maintain mouse holds. Only a handful of consumers of wintouchemu remain: 1. beatstream - but it's off by default, only enabled as errata for buggy touchscreens, or if the user forces it on (for Show Cursor) 2. gitadora single-window overlay - this is just for mouse so not a big deal. 3. MFC HG mode - not a priority to fix. ## Testing Tested full screen and windowed mode with touch / mouse / poke. --- src/spice2x/acio/core/core.cpp | 4 +- src/spice2x/games/nost/nost.cpp | 10 +- src/spice2x/games/nost/poke.cpp | 164 ++++++++++-------- src/spice2x/misc/wintouchemu.cpp | 32 +--- src/spice2x/misc/wintouchemu.h | 2 +- src/spice2x/touch/native/inject.cpp | 70 +++++++- src/spice2x/touch/native/inject_internal.h | 1 + src/spice2x/touch/native/inject_mouse.cpp | 13 +- src/spice2x/touch/native/inject_synthetic.cpp | 7 +- src/spice2x/touch/native/nativetouchhook.cpp | 51 +++++- src/spice2x/touch/native/nativetouchhook.h | 1 + src/spice2x/touch/native/settings.h | 7 + src/spice2x/touch/native/transform.cpp | 36 +++- 13 files changed, 270 insertions(+), 128 deletions(-) create mode 100644 src/spice2x/touch/native/settings.h diff --git a/src/spice2x/acio/core/core.cpp b/src/spice2x/acio/core/core.cpp index 5e6313c..117c64a 100644 --- a/src/spice2x/acio/core/core.cpp +++ b/src/spice2x/acio/core/core.cpp @@ -4,6 +4,7 @@ #include "launcher/launcher.h" #include "misc/wintouchemu.h" #include "rawinput/rawinput.h" +#include "touch/native/nativetouchhook.h" // static stuff static int ACIO_WARMUP = 0; @@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) { // flush device output RI_MGR->devices_flush_output(); - // update wintouchemu + // update touch emulation wintouchemu::update(); + nativetouch::refresh_contact_lifetime(); return 1; } diff --git a/src/spice2x/games/nost/nost.cpp b/src/spice2x/games/nost/nost.cpp index 0c87558..6175f13 100644 --- a/src/spice2x/games/nost/nost.cpp +++ b/src/spice2x/games/nost/nost.cpp @@ -1,11 +1,7 @@ -// enable touch functions - set version to windows 7 -// mingw otherwise doesn't load touch stuff -#define _WIN32_WINNT 0x0601 - #include "nost.h" #include "poke.h" #include "hooks/setupapihook.h" -#include "misc/wintouchemu.h" +#include "touch/native/nativetouchhook.h" #include "avs/game.h" namespace games::nost { @@ -43,9 +39,7 @@ namespace games::nost { setupapihook_init(avs::game::DLL_INSTANCE); setupapihook_add(touch_settings); - // custom touch - // nostalgia crashes if you inject touch events too early - wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20); + nativetouch::hook(avs::game::DLL_INSTANCE); } void NostGame::post_attach() { diff --git a/src/spice2x/games/nost/poke.cpp b/src/spice2x/games/nost/poke.cpp index 799a6e4..93e1794 100644 --- a/src/spice2x/games/nost/poke.cpp +++ b/src/spice2x/games/nost/poke.cpp @@ -1,22 +1,24 @@ #include "poke.h" -#include +#include #include +#include #include "games/nost/io.h" #include "cfg/api.h" #include "rawinput/rawinput.h" #include "misc/eamuse.h" -#include "touch/touch.h" +#include "touch/native/inject.h" #include "util/logging.h" #include "util/precise_timer.h" namespace games::nost::poke { static std::thread *THREAD = nullptr; - static volatile bool THREAD_RUNNING = false; - - static const std::unordered_map> NOST_POKE_NUM { + static std::atomic THREAD_RUNNING { false }; + + // positions use game-client pixels, matching the legacy client-sized touch window + static const std::unordered_map> NOST_POKE_KEYPAD { {EAM_IO_KEYPAD_0, {760, 440}}, {EAM_IO_KEYPAD_1, {760, 385}}, {EAM_IO_KEYPAD_2, {820, 385}}, @@ -31,7 +33,8 @@ namespace games::nost::poke { {EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button }; - static const std::unordered_map> NOST_POKE { + static const std::unordered_map< + games::nost::Buttons::Button, std::pair> NOST_POKE_BUTTONS { {games::nost::Buttons::Button::PokeConfirm, {1100, 525}}, {games::nost::Buttons::Button::PokeBack, {340, 100}}, {games::nost::Buttons::Button::PokeSong1, {450, 190}}, @@ -46,13 +49,8 @@ namespace games::nost::poke { {games::nost::Buttons::Button::PokeDifficulty4, {820, 490}}, }; - void clear_touch_points(std::vector *touch_points) { - std::vector touch_ids; - for (auto &touch : *touch_points) { - touch_ids.emplace_back(touch.id); - } - touch_remove_points(&touch_ids); - touch_points->clear(); + static bool inject_poke_position(POINT position, bool down) { + return nativetouch::inject::inject_synthetic_touch(position, down); } void enable() { @@ -65,124 +63,138 @@ namespace games::nost::poke { THREAD_RUNNING = true; THREAD = new std::thread([] { timeutils::PreciseSleepTimer timer; - const DWORD touch_id = (DWORD)(0xFFFFFFFE); const int swipe_anim_total_frames = 6; const int swipe_anim_y = 300; const int swipe_next_page_x_begin = 1120; - const int swipe_next_page_x_end = 1120-120; + const int swipe_next_page_x_end = 1120 - 120; - const int swipe_prev_page_x_begin = 280-120; + const int swipe_prev_page_x_begin = 280 - 120; const int swipe_prev_page_x_end = 280; int next_page_anim_index = -1; int prev_page_anim_index = -1; - std::vector touch_points; - - bool touch_release = false; + bool contact_active = false; + bool release_pending = false; + POINT last_position {}; + std::unordered_map button_states; + uint16_t last_keypad_state = 0; // log log_info("poke", "enabled"); // set variable to false to stop while (THREAD_RUNNING) { - // clean up touch from last frame - if (!touch_points.empty()) { - if (touch_release) { - clear_touch_points(&touch_points); - touch_release = false; - } else { - touch_points.clear(); + if (release_pending) { + if (contact_active) { + inject_poke_position(last_position, false); } + contact_active = false; + release_pending = false; } auto &buttons = games::nost::get_buttons(); + std::optional touch_position; + bool release_after_touch = false; + + const auto button_triggered = [&](games::nost::Buttons::Button button) { + const auto pressed = + GameAPI::Buttons::getState(RI_MGR, buttons.at(button)); + const auto triggered = pressed && !button_states[button]; + button_states[button] = pressed; + return triggered; + }; + + std::optional triggered_touch_position; + for (const auto& it : NOST_POKE_BUTTONS) { + if (button_triggered(it.first) && !triggered_touch_position.has_value()) { + triggered_touch_position = POINT { + it.second.first, + it.second.second, + }; + } + } + + const auto next_triggered = + button_triggered(games::nost::Buttons::PokeNextPage); + const auto prev_triggered = + button_triggered(games::nost::Buttons::PokePrevPage); + + const auto keypad_state = eamuse_get_keypad_state(0); + if (!triggered_touch_position.has_value()) { + for (const auto& it : NOST_POKE_KEYPAD) { + const auto mask = static_cast(1 << it.first); + if ((keypad_state & mask) && !(last_keypad_state & mask)) { + triggered_touch_position = POINT { + it.second.first, + it.second.second, + }; + break; + } + } + } + last_keypad_state = keypad_state; if (0 <= next_page_anim_index) { const auto delta = (swipe_next_page_x_end - swipe_next_page_x_begin) * (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames; - TouchPoint tp { - .id = touch_id, - .x = (LONG)swipe_next_page_x_begin + delta, - .y = (LONG)swipe_anim_y, - .mouse = true, + touch_position = POINT { + swipe_next_page_x_begin + delta, + swipe_anim_y, }; - touch_points.emplace_back(tp); next_page_anim_index--; if (next_page_anim_index < 0) { - touch_release = true; + release_after_touch = true; } } else if (0 <= prev_page_anim_index) { const auto delta = (swipe_prev_page_x_end - swipe_prev_page_x_begin) * (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames; - TouchPoint tp { - .id = touch_id, - .x = (LONG)swipe_prev_page_x_begin + delta, - .y = (LONG)swipe_anim_y, - .mouse = true, + touch_position = POINT { + swipe_prev_page_x_begin + delta, + swipe_anim_y, }; - touch_points.emplace_back(tp); prev_page_anim_index--; if (prev_page_anim_index < 0) { - touch_release = true; + release_after_touch = true; } } else { - for (const auto& it : NOST_POKE) { - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) { - TouchPoint tp { - .id = touch_id, - .x = it.second.first, - .y = it.second.second, - .mouse = true, - }; - touch_points.emplace_back(tp); - touch_release = true; - break; - } - } - if (!touch_release) { - const auto state = eamuse_get_keypad_state(0); - if (state) { - for (const auto& it : NOST_POKE_NUM) { - if (state & (1 << it.first)) { - TouchPoint tp { - .id = touch_id, - .x = it.second.first, - .y = it.second.second, - .mouse = true, - }; - touch_points.emplace_back(tp); - touch_release = true; - break; - } - } - } - } + touch_position = triggered_touch_position; + release_after_touch = touch_position.has_value(); // start animations for next frame - if (!touch_release) { - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) { + if (!touch_position.has_value()) { + if (next_triggered) { next_page_anim_index = swipe_anim_total_frames; } - if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) { + + if (prev_triggered) { prev_page_anim_index = swipe_anim_total_frames; } } } - if (!touch_points.empty()) { - touch_write_points(&touch_points); + if (touch_position.has_value() && + inject_poke_position(*touch_position, true)) { + contact_active = true; + last_position = *touch_position; + } + if (release_after_touch && contact_active) { + release_pending = true; } // slow down timer.sleep(30); } + if (contact_active) { + inject_poke_position(last_position, false); + } + return nullptr; }); } diff --git a/src/spice2x/misc/wintouchemu.cpp b/src/spice2x/misc/wintouchemu.cpp index 6418f93..8d419b3 100644 --- a/src/spice2x/misc/wintouchemu.cpp +++ b/src/spice2x/misc/wintouchemu.cpp @@ -4,10 +4,6 @@ #include "wintouchemu.h" -#include -#include -#include - #include "games/gitadora/gitadora.h" #include "hooks/graphics/graphics.h" #include "overlay/overlay.h" @@ -78,10 +74,10 @@ namespace wintouchemu { std::vector TOUCH_POINTS; HMODULE HOOKED_MODULE = nullptr; std::string WINDOW_TITLE_START = ""; - volatile bool INITIALIZED = false; + bool INITIALIZED = false; mouse_state_t mouse_state; - void hook(const char *window_title, HMODULE module, int delay_in_s) { + void hook(const char *window_title, HMODULE module) { // hooks auto system_metrics_hook = detour::iat_try( @@ -100,20 +96,8 @@ namespace wintouchemu { // 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; - } + log_misc("wintouchemu", "initializing"); + INITIALIZED = true; } static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { @@ -275,12 +259,8 @@ namespace wintouchemu { } } 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. - */ - + // 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) { diff --git a/src/spice2x/misc/wintouchemu.h b/src/spice2x/misc/wintouchemu.h index e8f6b33..6592913 100644 --- a/src/spice2x/misc/wintouchemu.h +++ b/src/spice2x/misc/wintouchemu.h @@ -10,6 +10,6 @@ namespace wintouchemu { extern bool LOG_FPS; extern bool ADD_TOUCH_FLAG_PRIMARY; - void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0); + void hook(const char *window_title, HMODULE module = nullptr); void update(); } diff --git a/src/spice2x/touch/native/inject.cpp b/src/spice2x/touch/native/inject.cpp index 3342889..64bd6a7 100644 --- a/src/spice2x/touch/native/inject.cpp +++ b/src/spice2x/touch/native/inject.cpp @@ -114,11 +114,18 @@ namespace nativetouch::inject { // normally the active touch window, while dedicated TDJ uses the subscreen window static std::atomic injection_window { nullptr }; + // cross-thread game-loop refreshes are coalesced so they cannot flood the window queue + static std::atomic contact_refresh_pending { false }; + static std::once_flag initialization_once; static int window_subclass_token; + // asks the touch window thread to send an UPDATE when the game loop runs elsewhere + static UINT contact_refresh_message; + // submit one synthetic contact frame to Windows touch injection - static bool inject_touch_frame(POINT position, POINTER_FLAGS pointer_flags) { + static bool inject_touch_frame( + POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) { POINTER_TOUCH_INFO contact {}; contact.pointerInfo.pointerType = PT_TOUCH; contact.pointerInfo.pointerId = 0; @@ -131,13 +138,14 @@ namespace nativetouch::inject { return true; } - // if UPDATE fails, drop it + // drop ordinary UPDATE frames when Windows is still processing the prior frame const auto error = GetLastError(); - if (error == ERROR_NOT_READY && (pointer_flags & POINTER_FLAG_UPDATE)) { + if (error == ERROR_NOT_READY && + (pointer_flags & POINTER_FLAG_UPDATE) && !retry_if_not_ready) { return true; } - // if DOWN or UP fails, retry once after a brief delay; if it still fails, log a warning + // retry required frames once after a brief delay if (error == ERROR_NOT_READY) { Sleep(INJECTION_RETRY_DELAY_MS); result = InjectTouchInput_ptr(1, &contact); @@ -222,6 +230,41 @@ namespace nativetouch::inject { return true; } + // contact state belongs to the injection window thread; this helper must run there + static void refresh_contact_lifetime_on_window_thread(HWND window) { + if (!contact_is_owned_by(contact_state.owner, window)) { + return; + } + + inject_touch_frame(contact_state.position, CONTACT_UPDATE_FLAGS, true); + } + + // PAN calls this from ac_io_update to keep a stationary contact alive. Contact state is + // owned by the touch window thread, so same-thread calls refresh immediately. Calls from + // another thread post one coalesced private message for the window thread to handle. + void refresh_contact_lifetime() { + const auto window = injection_window.load(std::memory_order_acquire); + if (window == nullptr) { + return; + } + + // preserve synchronous game-loop timing when it already runs on the window thread + const auto window_thread = GetWindowThreadProcessId(window, nullptr); + if (window_thread == GetCurrentThreadId()) { + refresh_contact_lifetime_on_window_thread(window); + return; + } + + // otherwise marshal one outstanding refresh instead of sharing contact state across threads + if (contact_refresh_message == 0 || + contact_refresh_pending.exchange(true, std::memory_order_acq_rel)) { + return; + } + if (!PostMessageW(window, contact_refresh_message, 0, 0)) { + contact_refresh_pending.store(false, std::memory_order_release); + } + } + void set_contact_timer( ContactOwner owner, HWND window, UINT_PTR timer_id) { if (contact_is_owned_by(owner, window)) { @@ -255,6 +298,13 @@ namespace nativetouch::inject { HWND window, UINT message, WPARAM w_param, LPARAM l_param, UINT_PTR subclass_id, DWORD_PTR) { + // consume marshalled lifetime refreshes on the contact-owning window thread + if (contact_refresh_message != 0 && message == contact_refresh_message) { + contact_refresh_pending.store(false, std::memory_order_release); + refresh_contact_lifetime_on_window_thread(window); + return 0; + } + // IIDX handles touch on its main window even when input belongs to the subscreen // forward the touch message there if (message == WM_TOUCH && @@ -278,6 +328,7 @@ namespace nativetouch::inject { HWND expected_window = window; injection_window.compare_exchange_strong( expected_window, nullptr, std::memory_order_acq_rel); + contact_refresh_pending.store(false, std::memory_order_release); touch_delivery_window.store(nullptr, std::memory_order_release); RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id); } @@ -313,7 +364,10 @@ namespace nativetouch::inject { touch_window_subclass_proc, reinterpret_cast(&window_subclass_token), 0)) { - log_warning("touch::native", "failed to attach mouse touch injection to window: {}", GetLastError()); + log_warning( + "touch::native", + "failed to attach mouse touch injection to window: {}", + GetLastError()); return; } @@ -371,6 +425,12 @@ namespace nativetouch::inject { void initialize_touch_injection() { std::call_once(initialization_once, [] { initialize_synthetic_touch(); + contact_refresh_message = + RegisterWindowMessageW(L"spice2x.native_touch.refresh_contact"); + if (contact_refresh_message == 0) { + log_warning( + "touch::native", "failed to register contact refresh message: {}", GetLastError()); + } // load all APIs from user32 without adding static imports const auto user32 = libutils::load_library("user32.dll"); diff --git a/src/spice2x/touch/native/inject_internal.h b/src/spice2x/touch/native/inject_internal.h index b519ec3..838248e 100644 --- a/src/spice2x/touch/native/inject_internal.h +++ b/src/spice2x/touch/native/inject_internal.h @@ -13,6 +13,7 @@ namespace nativetouch::inject { void initialize_touch_injection(); void initialize_synthetic_touch(); bool touch_injection_available(); + void refresh_contact_lifetime(); bool contact_is_active(); bool contact_is_owned_by(ContactOwner owner, HWND window); diff --git a/src/spice2x/touch/native/inject_mouse.cpp b/src/spice2x/touch/native/inject_mouse.cpp index 6958f6c..29f0a79 100644 --- a/src/spice2x/touch/native/inject_mouse.cpp +++ b/src/spice2x/touch/native/inject_mouse.cpp @@ -4,6 +4,7 @@ #include #include "inject_internal.h" +#include "settings.h" #include "transform.h" #include "touch/touch.h" @@ -64,11 +65,13 @@ namespace nativetouch::inject { // keep receiving drag messages after the cursor leaves the client area SetCapture(window); - const auto timer_id = reinterpret_cast(&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"); + if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) { + const auto timer_id = reinterpret_cast(&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"); + } } } diff --git a/src/spice2x/touch/native/inject_synthetic.cpp b/src/spice2x/touch/native/inject_synthetic.cpp index 447edd3..83f1b32 100644 --- a/src/spice2x/touch/native/inject_synthetic.cpp +++ b/src/spice2x/touch/native/inject_synthetic.cpp @@ -8,6 +8,7 @@ #include "inject.h" #include "inject_internal.h" +#include "settings.h" #include "transform.h" #include "util/logging.h" @@ -38,10 +39,10 @@ namespace nativetouch::inject { // synthetic touches preempt the mouse and keep it disabled until release or timeout static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) { - // dedicated TDJ injects into the physical subscreen, so map Windows' - // returned coordinates back into the game's touch coordinate space + // remember when Windows-returned coordinates must map back into game space const auto transform_returned_coordinates = - transform::is_tdj_dedicated_subscreen(window); + transform::is_tdj_dedicated_subscreen(window) || + settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES; if (!screen_space && !transform::game_to_screen(window, &position)) { return; } diff --git a/src/spice2x/touch/native/nativetouchhook.cpp b/src/spice2x/touch/native/nativetouchhook.cpp index ef0a43f..855c023 100644 --- a/src/spice2x/touch/native/nativetouchhook.cpp +++ b/src/spice2x/touch/native/nativetouchhook.cpp @@ -5,6 +5,8 @@ #include "avs/game.h" #include "rawinput/touch.h" #include "inject.h" +#include "inject_internal.h" +#include "settings.h" #include "transform.h" #include "util/detour.h" @@ -22,6 +24,13 @@ namespace nativetouch { + namespace settings { + bool EMULATE_DIGITIZER = false; + bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP = false; + bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES = false; + } + + static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr; static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; static bool native_touch_hooked = false; static bool native_display_initialized = false; @@ -29,6 +38,28 @@ namespace nativetouch { static long native_display_size_x = 1920L; static long native_display_size_y = 1080L; + static void initialize_game_settings() { + const auto is_pan = avs::game::is_model("PAN"); + settings::EMULATE_DIGITIZER = is_pan; + + // PAN samples and clears touch events in its I/O update loop. Window-timer updates + // made stationary holds flicker while movement updates remained stable, so refresh + // the native contact from ac_io_update instead of using the generic mouse timer. + settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP = is_pan; + + // translate native touch between desktop and game-window client coordinates + settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES = is_pan; + } + + static int WINAPI GetSystemMetricsHook(int index) { + if (index == SM_DIGITIZER) { + return NID_INTEGRATED_TOUCH | NID_EXTERNAL_TOUCH | + NID_MULTI_INPUT | NID_READY; + } + + return GetSystemMetrics_orig(index); + } + static void update_native_display_mode() { RECT display_rect{}; if (GetWindowRect(GetDesktopWindow(), &display_rect)) { @@ -102,13 +133,13 @@ namespace nativetouch { static BOOL WINAPI GetTouchInputInfoHook( HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { - // Refresh after exclusive fullscreen establishes the final display mode. + // 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 first const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize); if (result == 0) { return result; @@ -162,11 +193,27 @@ namespace nativetouch { return native_touch_hooked; } + void refresh_contact_lifetime() { + if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) { + inject::refresh_contact_lifetime(); + } + } + void hook(HMODULE module) { + initialize_game_settings(); + inject::hook(module); native_touch_hooked = true; + if (settings::EMULATE_DIGITIZER) { + GetSystemMetrics_orig = detour::iat_try( + "GetSystemMetrics", GetSystemMetricsHook, module); + if (GetSystemMetrics_orig != nullptr) { + log_misc("touch::native", "GetSystemMetrics hooked"); + } + } + GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); if (GetTouchInputInfo_orig != nullptr) { log_misc("touch::native", "GetTouchInputInfo hooked"); diff --git a/src/spice2x/touch/native/nativetouchhook.h b/src/spice2x/touch/native/nativetouchhook.h index fad821f..afbc1f5 100644 --- a/src/spice2x/touch/native/nativetouchhook.h +++ b/src/spice2x/touch/native/nativetouchhook.h @@ -2,6 +2,7 @@ namespace nativetouch { void hook(HMODULE module); + void refresh_contact_lifetime(); // true once hook() has installed the native touch stack for the current game; // such games consume touch through the GetTouchInputInfo hook rather than spicetouch diff --git a/src/spice2x/touch/native/settings.h b/src/spice2x/touch/native/settings.h new file mode 100644 index 0000000..08ac563 --- /dev/null +++ b/src/spice2x/touch/native/settings.h @@ -0,0 +1,7 @@ +#pragma once + +namespace nativetouch::settings { + extern bool EMULATE_DIGITIZER; + extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP; + extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES; +} \ No newline at end of file diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp index ce2eb38..35140df 100644 --- a/src/spice2x/touch/native/transform.cpp +++ b/src/spice2x/touch/native/transform.cpp @@ -2,17 +2,47 @@ #include "hooks/graphics/graphics.h" #include "overlay/overlay.h" +#include "settings.h" #include "touch/touch.h" namespace nativetouch::transform { + static bool game_client_to_screen(HWND window, POINT *position) { + RECT client_rect {}; + if (window == nullptr || + !GetClientRect(window, &client_rect) || + client_rect.right <= 0 || client_rect.bottom <= 0 || + !PtInRect(&client_rect, *position)) { + return false; + } + + return ClientToScreen(window, position) != FALSE; + } + + static bool screen_to_game_client(HWND window, POINT *position) { + RECT client_rect {}; + if (window == nullptr || + !GetClientRect(window, &client_rect) || + client_rect.right <= 0 || client_rect.bottom <= 0 || + !ScreenToClient(window, position) || + !PtInRect(&client_rect, *position)) { + return false; + } + + return true; + } + 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 + // convert game touch coordinates to Windows desktop coordinates bool game_to_screen(HWND window, POINT *position) { + if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) { + return game_client_to_screen(window, position); + } + if (!is_tdj_dedicated_subscreen(window)) { return true; } @@ -54,6 +84,10 @@ namespace nativetouch::transform { // convert physical screen coordinates to game touch coordinates for a known target bool screen_to_game(HWND window, POINT *position) { + if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) { + return screen_to_game_client(window, position); + } + // scale the resized IIDX subscreen client area into the game's touch-display coordinates if (is_tdj_dedicated_subscreen(window)) { RECT client_rect {};