mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
touch: inject touches from spiceapi to native touch hooks (#824)
## Link to GitHub Issue or related Pull Request, if one exists Fixes #814 ## Description of change Touches via the API wasn't being delivered to native touch hook; implement this, which closes the last gap we have vs. wintouchemu. While we're here, implement popn subscreen support for companion as well. ## Testing Tested for iidx/sdvx/popn
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,6 +10,8 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
@@ -17,6 +21,25 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool inject_native_touch(
|
||||
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||
if (touch_points.empty()) {
|
||||
// no active touches remain; release the synthetic contact
|
||||
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||
}
|
||||
|
||||
// only a single synthetic contact is supported, so just use the first touch point
|
||||
const auto &touch = touch_points.front();
|
||||
POINT position { touch.x, touch.y };
|
||||
if (canvas_w > 0 && canvas_h > 0) {
|
||||
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||
position,
|
||||
{ canvas_w, canvas_h },
|
||||
true);
|
||||
}
|
||||
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +49,25 @@ namespace api::modules {
|
||||
is_tdj_fhd = false;
|
||||
}
|
||||
|
||||
use_native = nativetouch::is_hooked();
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||
native_canvas_w = 0;
|
||||
native_canvas_h = 0;
|
||||
if (is_sdvx) {
|
||||
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
||||
native_canvas_w = 1080;
|
||||
native_canvas_h = 1920;
|
||||
} else if (avs::game::is_model("LDJ")) {
|
||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||
} else if (avs::game::is_model("M39")) {
|
||||
// pop'n music API touch surface
|
||||
native_canvas_w = 1280;
|
||||
native_canvas_h = 800;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,8 +141,12 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* write_reset(id: uint, ...)
|
||||
@@ -123,8 +169,44 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
touch_remove_points(&touch_point_ids);
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
const auto touch_id = touch_points.front().id;
|
||||
if (native_touch_id && *native_touch_id != touch_id) {
|
||||
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
return;
|
||||
}
|
||||
native_touch_id.reset();
|
||||
}
|
||||
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id = touch_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
if (!native_touch_id ||
|
||||
std::find(
|
||||
touch_point_ids.begin(),
|
||||
touch_point_ids.end(),
|
||||
*native_touch_id) == touch_point_ids.end()) {
|
||||
return;
|
||||
}
|
||||
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::apply_touch_errata(int &x, int &y) {
|
||||
int x_raw = x;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
bool is_tdj_fhd;
|
||||
bool use_native;
|
||||
std::mutex native_touch_mutex;
|
||||
std::optional<uint32_t> native_touch_id;
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||
// used to normalize before mapping into the native injection window
|
||||
int native_canvas_w;
|
||||
int native_canvas_h;
|
||||
|
||||
// function definitions
|
||||
void read(Request &req, Response &res);
|
||||
void write(Request &req, Response &res);
|
||||
void write_reset(Request &req, Response &res);
|
||||
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||
void apply_touch_errata(int &x, int &y);
|
||||
|
||||
};
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace nativetouch::inject {
|
||||
void register_and_attach_window(HWND window);
|
||||
void 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);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ namespace nativetouch::inject {
|
||||
|
||||
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
||||
|
||||
enum class SyntheticTouchMessage : WPARAM {
|
||||
Up, // used by callers releasing a contact
|
||||
DownGameSpace, // coordinates are relative to the game's logical touch surface
|
||||
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
|
||||
};
|
||||
|
||||
static std::once_flag synthetic_initialization_once;
|
||||
static int synthetic_contact_timer_token;
|
||||
static UINT synthetic_touch_message;
|
||||
@@ -31,14 +37,29 @@ 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) {
|
||||
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
|
||||
const auto transform_returned_coordinates =
|
||||
transform::is_tdj_dedicated_subscreen(window);
|
||||
if (!transform::game_to_screen(window, &position)) {
|
||||
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||
|
||||
// when this producer already owns the contact, move it instead of releasing and
|
||||
// re-pressing so continuous input (such as the API surface) drags smoothly
|
||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||
if (update_contact(ContactOwner::Synthetic, window, position)) {
|
||||
// refresh the safety timeout while updates keep arriving
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!release_active_contact()) {
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +71,6 @@ namespace nativetouch::inject {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||
} else {
|
||||
@@ -68,10 +88,16 @@ namespace nativetouch::inject {
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
||||
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
||||
if (w_param) {
|
||||
begin_synthetic_contact(window, position);
|
||||
} else {
|
||||
switch (static_cast<SyntheticTouchMessage>(w_param)) {
|
||||
case SyntheticTouchMessage::DownGameSpace:
|
||||
begin_synthetic_contact(window, position, false);
|
||||
break;
|
||||
case SyntheticTouchMessage::DownScreenSpace:
|
||||
begin_synthetic_contact(window, position, true);
|
||||
break;
|
||||
default:
|
||||
end_synthetic_contact(window);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -85,20 +111,63 @@ namespace nativetouch::inject {
|
||||
return false;
|
||||
}
|
||||
|
||||
// inject synthetic touches; only one contact is supported at a time for simplicity
|
||||
bool inject_synthetic_touch(POINT position, bool down) {
|
||||
static HWND prepare_synthetic_touch() {
|
||||
initialize_touch_injection();
|
||||
|
||||
const auto window = get_injection_window();
|
||||
if (!touch_injection_available() || window == nullptr ||
|
||||
synthetic_touch_message == 0) {
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
static bool post_synthetic_touch(
|
||||
HWND window, POINT position, SyntheticTouchMessage message) {
|
||||
return PostMessageW(
|
||||
window,
|
||||
synthetic_touch_message,
|
||||
static_cast<WPARAM>(down),
|
||||
static_cast<WPARAM>(message),
|
||||
MAKELPARAM(position.x, position.y)) != FALSE;
|
||||
}
|
||||
|
||||
// inject a point expressed in the game's synthetic touch coordinate space
|
||||
bool inject_synthetic_touch(POINT position, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto message = down
|
||||
? SyntheticTouchMessage::DownGameSpace
|
||||
: SyntheticTouchMessage::Up;
|
||||
return post_synthetic_touch(window, position, message);
|
||||
}
|
||||
|
||||
// map a logical canvas point onto the live injection window before injecting it
|
||||
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
|
||||
const auto window = prepare_synthetic_touch();
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!down) {
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
|
||||
}
|
||||
|
||||
RECT client_rect {};
|
||||
if (canvas.cx <= 0 || canvas.cy <= 0 ||
|
||||
!GetClientRect(window, &client_rect) ||
|
||||
client_rect.right <= 0 || client_rect.bottom <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
|
||||
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
|
||||
if (!ClientToScreen(window, &position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
namespace nativetouch {
|
||||
|
||||
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||
static bool native_touch_hooked = false;
|
||||
static bool native_display_initialized = false;
|
||||
static DWORD native_display_orientation = DMDO_DEFAULT;
|
||||
static long native_display_size_x = 1920L;
|
||||
@@ -157,9 +158,15 @@ namespace nativetouch {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool is_hooked() {
|
||||
return native_touch_hooked;
|
||||
}
|
||||
|
||||
void hook(HMODULE module) {
|
||||
inject::hook(module);
|
||||
|
||||
native_touch_hooked = true;
|
||||
|
||||
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
||||
if (GetTouchInputInfo_orig != nullptr) {
|
||||
log_misc("touch::native", "GetTouchInputInfo hooked");
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
|
||||
namespace nativetouch {
|
||||
void hook(HMODULE module);
|
||||
|
||||
// true once hook() has installed the native touch stack for the current game;
|
||||
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch
|
||||
bool is_hooked();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user