mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
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.
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
|
||||||
// static stuff
|
// static stuff
|
||||||
static int ACIO_WARMUP = 0;
|
static int ACIO_WARMUP = 0;
|
||||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
|||||||
// flush device output
|
// flush device output
|
||||||
RI_MGR->devices_flush_output();
|
RI_MGR->devices_flush_output();
|
||||||
|
|
||||||
// update wintouchemu
|
// update touch emulation
|
||||||
wintouchemu::update();
|
wintouchemu::update();
|
||||||
|
nativetouch::refresh_contact_lifetime();
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "nost.h"
|
||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
#include "hooks/setupapihook.h"
|
#include "hooks/setupapihook.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
|
||||||
namespace games::nost {
|
namespace games::nost {
|
||||||
@@ -43,9 +39,7 @@ namespace games::nost {
|
|||||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||||
setupapihook_add(touch_settings);
|
setupapihook_add(touch_settings);
|
||||||
|
|
||||||
// custom touch
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
// nostalgia crashes if you inject touch events too early
|
|
||||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NostGame::post_attach() {
|
void NostGame::post_attach() {
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
|
||||||
#include <thread>
|
#include <atomic>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "games/nost/io.h"
|
#include "games/nost/io.h"
|
||||||
#include "cfg/api.h"
|
#include "cfg/api.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/native/inject.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
namespace games::nost::poke {
|
namespace games::nost::poke {
|
||||||
|
|
||||||
static std::thread *THREAD = nullptr;
|
static std::thread *THREAD = nullptr;
|
||||||
static volatile bool THREAD_RUNNING = false;
|
static std::atomic<bool> THREAD_RUNNING { false };
|
||||||
|
|
||||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
// positions use game-client pixels, matching the legacy client-sized touch window
|
||||||
|
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
|
||||||
{EAM_IO_KEYPAD_0, {760, 440}},
|
{EAM_IO_KEYPAD_0, {760, 440}},
|
||||||
{EAM_IO_KEYPAD_1, {760, 385}},
|
{EAM_IO_KEYPAD_1, {760, 385}},
|
||||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||||
@@ -31,7 +33,8 @@ namespace games::nost::poke {
|
|||||||
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
|
static const std::unordered_map<
|
||||||
|
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
|
||||||
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
||||||
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
||||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||||
@@ -46,13 +49,8 @@ namespace games::nost::poke {
|
|||||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||||
};
|
};
|
||||||
|
|
||||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
static bool inject_poke_position(POINT position, bool down) {
|
||||||
std::vector<DWORD> touch_ids;
|
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
touch_ids.emplace_back(touch.id);
|
|
||||||
}
|
|
||||||
touch_remove_points(&touch_ids);
|
|
||||||
touch_points->clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void enable() {
|
||||||
@@ -65,7 +63,6 @@ namespace games::nost::poke {
|
|||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
timeutils::PreciseSleepTimer timer;
|
timeutils::PreciseSleepTimer timer;
|
||||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
|
||||||
|
|
||||||
const int swipe_anim_total_frames = 6;
|
const int swipe_anim_total_frames = 6;
|
||||||
const int swipe_anim_y = 300;
|
const int swipe_anim_y = 300;
|
||||||
@@ -78,111 +75,126 @@ namespace games::nost::poke {
|
|||||||
|
|
||||||
int next_page_anim_index = -1;
|
int next_page_anim_index = -1;
|
||||||
int prev_page_anim_index = -1;
|
int prev_page_anim_index = -1;
|
||||||
std::vector<TouchPoint> touch_points;
|
bool contact_active = false;
|
||||||
|
bool release_pending = false;
|
||||||
bool touch_release = false;
|
POINT last_position {};
|
||||||
|
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||||
|
uint16_t last_keypad_state = 0;
|
||||||
|
|
||||||
// log
|
// log
|
||||||
log_info("poke", "enabled");
|
log_info("poke", "enabled");
|
||||||
|
|
||||||
// set variable to false to stop
|
// set variable to false to stop
|
||||||
while (THREAD_RUNNING) {
|
while (THREAD_RUNNING) {
|
||||||
// clean up touch from last frame
|
if (release_pending) {
|
||||||
if (!touch_points.empty()) {
|
if (contact_active) {
|
||||||
if (touch_release) {
|
inject_poke_position(last_position, false);
|
||||||
clear_touch_points(&touch_points);
|
|
||||||
touch_release = false;
|
|
||||||
} else {
|
|
||||||
touch_points.clear();
|
|
||||||
}
|
}
|
||||||
|
contact_active = false;
|
||||||
|
release_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &buttons = games::nost::get_buttons();
|
auto &buttons = games::nost::get_buttons();
|
||||||
|
std::optional<POINT> 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<POINT> 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<uint16_t>(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) {
|
if (0 <= next_page_anim_index) {
|
||||||
const auto delta =
|
const auto delta =
|
||||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||||
|
|
||||||
TouchPoint tp {
|
touch_position = POINT {
|
||||||
.id = touch_id,
|
swipe_next_page_x_begin + delta,
|
||||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
swipe_anim_y,
|
||||||
.y = (LONG)swipe_anim_y,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
};
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
next_page_anim_index--;
|
next_page_anim_index--;
|
||||||
if (next_page_anim_index < 0) {
|
if (next_page_anim_index < 0) {
|
||||||
touch_release = true;
|
release_after_touch = true;
|
||||||
}
|
}
|
||||||
} else if (0 <= prev_page_anim_index) {
|
} else if (0 <= prev_page_anim_index) {
|
||||||
const auto delta =
|
const auto delta =
|
||||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||||
|
|
||||||
TouchPoint tp {
|
touch_position = POINT {
|
||||||
.id = touch_id,
|
swipe_prev_page_x_begin + delta,
|
||||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
swipe_anim_y,
|
||||||
.y = (LONG)swipe_anim_y,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
};
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
prev_page_anim_index--;
|
prev_page_anim_index--;
|
||||||
if (prev_page_anim_index < 0) {
|
if (prev_page_anim_index < 0) {
|
||||||
touch_release = true;
|
release_after_touch = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const auto& it : NOST_POKE) {
|
touch_position = triggered_touch_position;
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
|
release_after_touch = touch_position.has_value();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start animations for next frame
|
// start animations for next frame
|
||||||
if (!touch_release) {
|
if (!touch_position.has_value()) {
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
if (next_triggered) {
|
||||||
next_page_anim_index = swipe_anim_total_frames;
|
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;
|
prev_page_anim_index = swipe_anim_total_frames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!touch_points.empty()) {
|
if (touch_position.has_value() &&
|
||||||
touch_write_points(&touch_points);
|
inject_poke_position(*touch_position, true)) {
|
||||||
|
contact_active = true;
|
||||||
|
last_position = *touch_position;
|
||||||
|
}
|
||||||
|
if (release_after_touch && contact_active) {
|
||||||
|
release_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(30);
|
timer.sleep(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contact_active) {
|
||||||
|
inject_poke_position(last_position, false);
|
||||||
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
|
|
||||||
#include "wintouchemu.h"
|
#include "wintouchemu.h"
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
@@ -78,10 +74,10 @@ namespace wintouchemu {
|
|||||||
std::vector<TouchPoint> TOUCH_POINTS;
|
std::vector<TouchPoint> TOUCH_POINTS;
|
||||||
HMODULE HOOKED_MODULE = nullptr;
|
HMODULE HOOKED_MODULE = nullptr;
|
||||||
std::string WINDOW_TITLE_START = "";
|
std::string WINDOW_TITLE_START = "";
|
||||||
volatile bool INITIALIZED = false;
|
bool INITIALIZED = false;
|
||||||
mouse_state_t mouse_state;
|
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
|
// hooks
|
||||||
auto system_metrics_hook = detour::iat_try(
|
auto system_metrics_hook = detour::iat_try(
|
||||||
@@ -100,21 +96,9 @@ namespace wintouchemu {
|
|||||||
// set module and title
|
// set module and title
|
||||||
HOOKED_MODULE = module;
|
HOOKED_MODULE = module;
|
||||||
WINDOW_TITLE_START = window_title;
|
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");
|
log_misc("wintouchemu", "initializing");
|
||||||
INITIALIZED = true;
|
INITIALIZED = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||||
|
|
||||||
@@ -275,12 +259,8 @@ namespace wintouchemu {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
/*
|
// beatstream requires a MOVE for active points in each update
|
||||||
* For some reason, Nostalgia won't show an active touch point unless a move event
|
// add one for every active point without a matching input 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
|
// find touch point which has no associated input event
|
||||||
TouchPoint *touch_point = nullptr;
|
TouchPoint *touch_point = nullptr;
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
for (auto &tp : TOUCH_POINTS) {
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ namespace wintouchemu {
|
|||||||
extern bool LOG_FPS;
|
extern bool LOG_FPS;
|
||||||
extern bool ADD_TOUCH_FLAG_PRIMARY;
|
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();
|
void update();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,11 +114,18 @@ namespace nativetouch::inject {
|
|||||||
// normally the active touch window, while dedicated TDJ uses the subscreen window
|
// normally the active touch window, while dedicated TDJ uses the subscreen window
|
||||||
static std::atomic<HWND> injection_window { nullptr };
|
static std::atomic<HWND> injection_window { nullptr };
|
||||||
|
|
||||||
|
// cross-thread game-loop refreshes are coalesced so they cannot flood the window queue
|
||||||
|
static std::atomic<bool> contact_refresh_pending { false };
|
||||||
|
|
||||||
static std::once_flag initialization_once;
|
static std::once_flag initialization_once;
|
||||||
static int window_subclass_token;
|
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
|
// 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 {};
|
POINTER_TOUCH_INFO contact {};
|
||||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
contact.pointerInfo.pointerType = PT_TOUCH;
|
||||||
contact.pointerInfo.pointerId = 0;
|
contact.pointerInfo.pointerId = 0;
|
||||||
@@ -131,13 +138,14 @@ namespace nativetouch::inject {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if UPDATE fails, drop it
|
// drop ordinary UPDATE frames when Windows is still processing the prior frame
|
||||||
const auto error = GetLastError();
|
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;
|
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) {
|
if (error == ERROR_NOT_READY) {
|
||||||
Sleep(INJECTION_RETRY_DELAY_MS);
|
Sleep(INJECTION_RETRY_DELAY_MS);
|
||||||
result = InjectTouchInput_ptr(1, &contact);
|
result = InjectTouchInput_ptr(1, &contact);
|
||||||
@@ -222,6 +230,41 @@ namespace nativetouch::inject {
|
|||||||
return true;
|
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(
|
void set_contact_timer(
|
||||||
ContactOwner owner, HWND window, UINT_PTR timer_id) {
|
ContactOwner owner, HWND window, UINT_PTR timer_id) {
|
||||||
if (contact_is_owned_by(owner, window)) {
|
if (contact_is_owned_by(owner, window)) {
|
||||||
@@ -255,6 +298,13 @@ namespace nativetouch::inject {
|
|||||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
|
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
|
||||||
UINT_PTR subclass_id, DWORD_PTR) {
|
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
|
// IIDX handles touch on its main window even when input belongs to the subscreen
|
||||||
// forward the touch message there
|
// forward the touch message there
|
||||||
if (message == WM_TOUCH &&
|
if (message == WM_TOUCH &&
|
||||||
@@ -278,6 +328,7 @@ namespace nativetouch::inject {
|
|||||||
HWND expected_window = window;
|
HWND expected_window = window;
|
||||||
injection_window.compare_exchange_strong(
|
injection_window.compare_exchange_strong(
|
||||||
expected_window, nullptr, std::memory_order_acq_rel);
|
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);
|
touch_delivery_window.store(nullptr, std::memory_order_release);
|
||||||
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
|
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
|
||||||
}
|
}
|
||||||
@@ -313,7 +364,10 @@ namespace nativetouch::inject {
|
|||||||
touch_window_subclass_proc,
|
touch_window_subclass_proc,
|
||||||
reinterpret_cast<UINT_PTR>(&window_subclass_token),
|
reinterpret_cast<UINT_PTR>(&window_subclass_token),
|
||||||
0)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +425,12 @@ namespace nativetouch::inject {
|
|||||||
void initialize_touch_injection() {
|
void initialize_touch_injection() {
|
||||||
std::call_once(initialization_once, [] {
|
std::call_once(initialization_once, [] {
|
||||||
initialize_synthetic_touch();
|
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
|
// load all APIs from user32 without adding static imports
|
||||||
const auto user32 = libutils::load_library("user32.dll");
|
const auto user32 = libutils::load_library("user32.dll");
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace nativetouch::inject {
|
|||||||
void initialize_touch_injection();
|
void initialize_touch_injection();
|
||||||
void initialize_synthetic_touch();
|
void initialize_synthetic_touch();
|
||||||
bool touch_injection_available();
|
bool touch_injection_available();
|
||||||
|
void refresh_contact_lifetime();
|
||||||
|
|
||||||
bool contact_is_active();
|
bool contact_is_active();
|
||||||
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "inject_internal.h"
|
#include "inject_internal.h"
|
||||||
|
#include "settings.h"
|
||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
@@ -64,6 +65,7 @@ namespace nativetouch::inject {
|
|||||||
|
|
||||||
// keep receiving drag messages after the cursor leaves the client area
|
// keep receiving drag messages after the cursor leaves the client area
|
||||||
SetCapture(window);
|
SetCapture(window);
|
||||||
|
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
||||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
||||||
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
||||||
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
||||||
@@ -71,6 +73,7 @@ namespace nativetouch::inject {
|
|||||||
log_warning("touch::native", "failed to start mouse touch injection timer");
|
log_warning("touch::native", "failed to start mouse touch injection timer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update the contact while the primary button remains held
|
// update the contact while the primary button remains held
|
||||||
static void move_mouse_contact(
|
static void move_mouse_contact(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "inject.h"
|
#include "inject.h"
|
||||||
#include "inject_internal.h"
|
#include "inject_internal.h"
|
||||||
|
#include "settings.h"
|
||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "util/logging.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
|
// 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) {
|
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
|
||||||
// dedicated TDJ injects into the physical subscreen, so map Windows'
|
// remember when Windows-returned coordinates must map back into game space
|
||||||
// returned coordinates back into the game's touch coordinate space
|
|
||||||
const auto transform_returned_coordinates =
|
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)) {
|
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
#include "inject.h"
|
#include "inject.h"
|
||||||
|
#include "inject_internal.h"
|
||||||
|
#include "settings.h"
|
||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -22,6 +24,13 @@
|
|||||||
|
|
||||||
namespace nativetouch {
|
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 decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||||
static bool native_touch_hooked = false;
|
static bool native_touch_hooked = false;
|
||||||
static bool native_display_initialized = 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_x = 1920L;
|
||||||
static long native_display_size_y = 1080L;
|
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() {
|
static void update_native_display_mode() {
|
||||||
RECT display_rect{};
|
RECT display_rect{};
|
||||||
if (GetWindowRect(GetDesktopWindow(), &display_rect)) {
|
if (GetWindowRect(GetDesktopWindow(), &display_rect)) {
|
||||||
@@ -102,13 +133,13 @@ namespace nativetouch {
|
|||||||
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.
|
// refresh after exclusive fullscreen establishes the final display mode
|
||||||
if (!native_display_initialized) {
|
if (!native_display_initialized) {
|
||||||
update_native_display_mode();
|
update_native_display_mode();
|
||||||
native_display_initialized = true;
|
native_display_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the original fist
|
// call the original first
|
||||||
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;
|
||||||
@@ -162,11 +193,27 @@ namespace nativetouch {
|
|||||||
return native_touch_hooked;
|
return native_touch_hooked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refresh_contact_lifetime() {
|
||||||
|
if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
||||||
|
inject::refresh_contact_lifetime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void hook(HMODULE module) {
|
void hook(HMODULE module) {
|
||||||
|
initialize_game_settings();
|
||||||
|
|
||||||
inject::hook(module);
|
inject::hook(module);
|
||||||
|
|
||||||
native_touch_hooked = true;
|
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);
|
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");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace nativetouch {
|
namespace nativetouch {
|
||||||
void hook(HMODULE module);
|
void hook(HMODULE module);
|
||||||
|
void refresh_contact_lifetime();
|
||||||
|
|
||||||
// true once hook() has installed the native touch stack for the current game;
|
// true once hook() has installed the native touch stack for the current game;
|
||||||
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch
|
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,17 +2,47 @@
|
|||||||
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "settings.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
|
||||||
namespace nativetouch::transform {
|
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) {
|
bool is_tdj_dedicated_subscreen(HWND window) {
|
||||||
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
||||||
window == TDJ_SUBSCREEN_WINDOW;
|
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) {
|
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)) {
|
if (!is_tdj_dedicated_subscreen(window)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -54,6 +84,10 @@ namespace nativetouch::transform {
|
|||||||
|
|
||||||
// convert physical screen coordinates to game touch coordinates for a known target
|
// convert physical screen coordinates to game touch coordinates for a known target
|
||||||
bool screen_to_game(HWND window, POINT *position) {
|
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
|
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
||||||
if (is_tdj_dedicated_subscreen(window)) {
|
if (is_tdj_dedicated_subscreen(window)) {
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
|
|||||||
Reference in New Issue
Block a user