mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 |
@@ -1924,7 +1924,7 @@ namespace avs {
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
property_search(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
|
||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||
{
|
||||
pwd_ = absolute(pwd);
|
||||
// spice2x
|
||||
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||
// prefixes, which makes directory_iterator() throw during startup
|
||||
// spice2x
|
||||
const auto &nativePwd = pwd.native();
|
||||
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||
UpdateFileRecords();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> 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();
|
||||
|
||||
|
||||
@@ -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<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> 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:
|
||||
|
||||
@@ -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([] {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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([] {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace games::popn {
|
||||
#endif
|
||||
|
||||
extern bool SHOW_PIKA_MONITOR_WARNING;
|
||||
extern bool NATIVE_TOUCH;
|
||||
|
||||
class POPNGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<LONG_PTR>(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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1791,10 +1791,10 @@ static const std::vector<OptionDefinition> 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",
|
||||
},
|
||||
|
||||
+401
-45
@@ -1,5 +1,6 @@
|
||||
#include "signal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <exception>
|
||||
|
||||
@@ -25,6 +26,7 @@
|
||||
#endif
|
||||
|
||||
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
|
||||
static LONG EXCEPTION_HANDLER_ACTIVE = 0;
|
||||
|
||||
namespace launcher::signal {
|
||||
|
||||
@@ -47,7 +49,7 @@ static std::string control_code(DWORD dwCtrlType) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
static std::string exception_code(const struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
V(EXCEPTION_ACCESS_VIOLATION);
|
||||
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||
@@ -77,6 +79,388 @@ static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
|
||||
#undef V
|
||||
|
||||
static const char *access_operation(ULONG_PTR operation) {
|
||||
switch (operation) {
|
||||
case 0:
|
||||
return "read";
|
||||
case 1:
|
||||
return "write";
|
||||
case 8:
|
||||
return "execute";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_state(DWORD state) {
|
||||
switch (state) {
|
||||
case MEM_COMMIT:
|
||||
return "committed";
|
||||
case MEM_FREE:
|
||||
return "free";
|
||||
case MEM_RESERVE:
|
||||
return "reserved";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_type(DWORD type) {
|
||||
switch (type) {
|
||||
case MEM_IMAGE:
|
||||
return "image";
|
||||
case MEM_MAPPED:
|
||||
return "mapped";
|
||||
case MEM_PRIVATE:
|
||||
return "private";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_protection(DWORD protection) {
|
||||
switch (protection & 0xff) {
|
||||
case PAGE_EXECUTE:
|
||||
return "execute";
|
||||
case PAGE_EXECUTE_READ:
|
||||
return "execute-read";
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
return "execute-read-write";
|
||||
case PAGE_EXECUTE_WRITECOPY:
|
||||
return "execute-write-copy";
|
||||
case PAGE_NOACCESS:
|
||||
return "no-access";
|
||||
case PAGE_READONLY:
|
||||
return "read-only";
|
||||
case PAGE_READWRITE:
|
||||
return "read-write";
|
||||
case PAGE_WRITECOPY:
|
||||
return "write-copy";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_parameters(const struct _EXCEPTION_RECORD *record) {
|
||||
const auto parameter_count = record->NumberParameters <= EXCEPTION_MAXIMUM_PARAMETERS ?
|
||||
record->NumberParameters : EXCEPTION_MAXIMUM_PARAMETERS;
|
||||
|
||||
log_warning("signal", "exception flags: 0x{:08x}, parameters: {}",
|
||||
record->ExceptionFlags, parameter_count);
|
||||
|
||||
for (DWORD parameter = 0; parameter < parameter_count; parameter++) {
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "exception parameter[{}]: {:016x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#else
|
||||
log_warning("signal", "exception parameter[{}]: {:08x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static bool query_memory(const void *address, MEMORY_BASIC_INFORMATION *memory) {
|
||||
return VirtualQuery(address, memory, sizeof(*memory)) == sizeof(*memory);
|
||||
}
|
||||
|
||||
static void log_memory_information(const char *label, const MEMORY_BASIC_INFORMATION &memory) {
|
||||
log_warning("signal",
|
||||
"{}: base={}, size=0x{:x}, allocation_base={}, state={} (0x{:x}), "
|
||||
"protect={} (0x{:x}), type={} (0x{:x})",
|
||||
label,
|
||||
fmt::ptr(memory.BaseAddress),
|
||||
memory.RegionSize,
|
||||
fmt::ptr(memory.AllocationBase),
|
||||
memory_state(memory.State),
|
||||
memory.State,
|
||||
memory_protection(memory.Protect),
|
||||
memory.Protect,
|
||||
memory_type(memory.Type),
|
||||
memory.Type);
|
||||
}
|
||||
|
||||
static void log_memory_region(const char *label, const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory)) {
|
||||
log_warning("signal", "{}: VirtualQuery failed for {}: 0x{:08x}",
|
||||
label, fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_memory_information(label, memory);
|
||||
}
|
||||
|
||||
static void log_adjacent_memory_regions(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION current {};
|
||||
if (!query_memory(address, ¤t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto current_base = reinterpret_cast<uintptr_t>(current.BaseAddress);
|
||||
if (current_base > 0) {
|
||||
MEMORY_BASIC_INFORMATION previous {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_base - 1), &previous)) {
|
||||
log_memory_information("fault memory previous", previous);
|
||||
}
|
||||
}
|
||||
|
||||
const auto current_end = current_base + current.RegionSize;
|
||||
if (current_end > current_base) {
|
||||
MEMORY_BASIC_INFORMATION next {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_end), &next)) {
|
||||
log_memory_information("fault memory next", next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_module(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) || memory.Type != MEM_IMAGE || memory.AllocationBase == nullptr) {
|
||||
log_warning("signal", "exception module: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
char module_path[MAX_PATH] {};
|
||||
const auto module = static_cast<HMODULE>(memory.AllocationBase);
|
||||
const auto module_base = reinterpret_cast<uintptr_t>(memory.AllocationBase);
|
||||
const auto exception_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto module_length = GetModuleFileNameA(module, module_path, sizeof(module_path));
|
||||
|
||||
if (module_length > 0) {
|
||||
const std::string module_name(module_path, std::min<size_t>(module_length, sizeof(module_path)));
|
||||
log_warning("signal", "exception module: '{}' base={}, rva=0x{:x}",
|
||||
module_name, fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
} else {
|
||||
log_warning("signal", "exception module: base={}, rva=0x{:x}",
|
||||
fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_instruction_bytes(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) ||
|
||||
memory.State != MEM_COMMIT ||
|
||||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
|
||||
log_warning("signal", "instruction bytes: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr size_t max_instruction_bytes = 16;
|
||||
const auto region_end = reinterpret_cast<uintptr_t>(memory.BaseAddress) + memory.RegionSize;
|
||||
const auto instruction_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto available = region_end > instruction_address ? region_end - instruction_address : 0;
|
||||
const auto byte_count = std::min(max_instruction_bytes, static_cast<size_t>(available));
|
||||
uint8_t bytes[max_instruction_bytes] {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (byte_count == 0 ||
|
||||
!ReadProcessMemory(GetCurrentProcess(), address, bytes, byte_count, &bytes_read)) {
|
||||
log_warning("signal", "instruction bytes: read failed at {}: 0x{:08x}",
|
||||
fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string byte_string;
|
||||
byte_string.reserve(bytes_read * 3);
|
||||
|
||||
for (size_t index = 0; index < bytes_read; index++) {
|
||||
fmt::format_to(std::back_inserter(byte_string), "{:02x}{}",
|
||||
bytes[index], index + 1 < bytes_read ? " " : "");
|
||||
}
|
||||
|
||||
log_warning("signal", "instruction bytes: {}", byte_string);
|
||||
}
|
||||
|
||||
static void log_stack_context(const CONTEXT *context) {
|
||||
const auto *tib = reinterpret_cast<const NT_TIB *>(NtCurrentTeb());
|
||||
const auto stack_base = reinterpret_cast<uintptr_t>(tib->StackBase);
|
||||
const auto stack_limit = reinterpret_cast<uintptr_t>(tib->StackLimit);
|
||||
#ifdef _WIN64
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Rsp);
|
||||
#else
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Esp);
|
||||
#endif
|
||||
const auto stack_used = stack_base > stack_pointer ? stack_base - stack_pointer : 0;
|
||||
const auto stack_remaining = stack_pointer > stack_limit ? stack_pointer - stack_limit : 0;
|
||||
|
||||
log_warning("signal", "stack: base={}, limit={}, pointer={}, used=0x{:x}, remaining=0x{:x}",
|
||||
fmt::ptr(tib->StackBase),
|
||||
fmt::ptr(tib->StackLimit),
|
||||
fmt::ptr(reinterpret_cast<const void *>(stack_pointer)),
|
||||
stack_used,
|
||||
stack_remaining);
|
||||
}
|
||||
|
||||
static void log_exception_context(struct _EXCEPTION_POINTERS *ExceptionInfo, DWORD last_error) {
|
||||
const auto *record = ExceptionInfo->ExceptionRecord;
|
||||
const auto *context = ExceptionInfo->ContextRecord;
|
||||
|
||||
log_warning("signal", "thread id: {}", GetCurrentThreadId());
|
||||
log_warning("signal", "thread last error (GetLastError()) at exception: 0x{:08x}", last_error);
|
||||
log_warning("signal", "exception address: {}", fmt::ptr(record->ExceptionAddress));
|
||||
log_exception_module(record->ExceptionAddress);
|
||||
log_instruction_bytes(record->ExceptionAddress);
|
||||
log_exception_parameters(record);
|
||||
|
||||
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
|
||||
record->NumberParameters >= 2) {
|
||||
const auto operation = record->ExceptionInformation[0];
|
||||
const auto address = reinterpret_cast<const void *>(record->ExceptionInformation[1]);
|
||||
log_warning("signal", "invalid memory access: {} at {}",
|
||||
access_operation(operation), fmt::ptr(address));
|
||||
log_memory_region("fault memory", address);
|
||||
log_adjacent_memory_regions(address);
|
||||
}
|
||||
|
||||
if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) {
|
||||
log_warning("signal", "in-page error status: 0x{:08x}",
|
||||
static_cast<DWORD>(record->ExceptionInformation[2]));
|
||||
}
|
||||
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_stack_context(context);
|
||||
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "registers: rax={:016x} rbx={:016x} rcx={:016x} rdx={:016x}",
|
||||
context->Rax, context->Rbx, context->Rcx, context->Rdx);
|
||||
log_warning("signal", "registers: rsi={:016x} rdi={:016x} rbp={:016x} rsp={:016x}",
|
||||
context->Rsi, context->Rdi, context->Rbp, context->Rsp);
|
||||
log_warning("signal", "registers: r8={:016x} r9={:016x} r10={:016x} r11={:016x}",
|
||||
context->R8, context->R9, context->R10, context->R11);
|
||||
log_warning("signal", "registers: r12={:016x} r13={:016x} r14={:016x} r15={:016x}",
|
||||
context->R12, context->R13, context->R14, context->R15);
|
||||
log_warning("signal", "registers: rip={:016x} eflags={:08x}",
|
||||
context->Rip, context->EFlags);
|
||||
#else
|
||||
log_warning("signal", "registers: eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}",
|
||||
context->Eax, context->Ebx, context->Ecx, context->Edx);
|
||||
log_warning("signal", "registers: esi={:08x} edi={:08x} ebp={:08x} esp={:08x}",
|
||||
context->Esi, context->Edi, context->Ebp, context->Esp);
|
||||
log_warning("signal", "registers: eip={:08x} eflags={:08x}",
|
||||
context->Eip, context->EFlags);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void write_minidump(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
if (MiniDumpWriteDump_local == nullptr) {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file == INVALID_HANDLE_VALUE) {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
constexpr auto extended_type = static_cast<MINIDUMP_TYPE>(
|
||||
MiniDumpWithUnloadedModules |
|
||||
MiniDumpWithIndirectlyReferencedMemory |
|
||||
MiniDumpWithProcessThreadData |
|
||||
MiniDumpWithFullMemoryInfo |
|
||||
MiniDumpWithThreadInfo |
|
||||
MiniDumpIgnoreInaccessibleMemory);
|
||||
|
||||
auto written_type = extended_type;
|
||||
auto minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
extended_type,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
auto minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
|
||||
if (!minidump_written) {
|
||||
log_warning("signal", "failed to write extended minidump: 0x{:08x}; retrying MiniDumpNormal",
|
||||
minidump_error);
|
||||
|
||||
LARGE_INTEGER file_start {};
|
||||
if (SetFilePointerEx(minidump_file, file_start, nullptr, FILE_BEGIN) &&
|
||||
SetEndOfFile(minidump_file)) {
|
||||
written_type = MiniDumpNormal;
|
||||
minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
} else {
|
||||
minidump_error = GetLastError();
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_INTEGER minidump_size {};
|
||||
const auto minidump_size_available = minidump_written &&
|
||||
GetFileSizeEx(minidump_file, &minidump_size);
|
||||
CloseHandle(minidump_file);
|
||||
|
||||
if (minidump_written) {
|
||||
if (minidump_size_available) {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x}, size={} bytes)",
|
||||
static_cast<unsigned>(written_type), minidump_size.QuadPart);
|
||||
} else {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x})",
|
||||
static_cast<unsigned>(written_type));
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "failed to write 'minidump.dmp': 0x{:08x}", minidump_error);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_chain(const struct _EXCEPTION_RECORD *record) {
|
||||
constexpr size_t max_exception_chain_depth = 8;
|
||||
auto *record_cause = record->ExceptionRecord;
|
||||
size_t depth = 0;
|
||||
|
||||
while (record_cause != nullptr && depth < max_exception_chain_depth) {
|
||||
struct _EXCEPTION_RECORD cause {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (!ReadProcessMemory(
|
||||
GetCurrentProcess(),
|
||||
record_cause,
|
||||
&cause,
|
||||
sizeof(cause),
|
||||
&bytes_read) ||
|
||||
bytes_read != sizeof(cause)) {
|
||||
log_warning("signal", "failed to read exception chain record {} at {}: 0x{:08x}",
|
||||
depth, fmt::ptr(record_cause), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_warning("signal", "caused by: {} at {}",
|
||||
exception_code(&cause), fmt::ptr(cause.ExceptionAddress));
|
||||
log_exception_parameters(&cause);
|
||||
record_cause = cause.ExceptionRecord;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (record_cause != nullptr) {
|
||||
log_warning("signal", "exception chain truncated after {} records", max_exception_chain_depth);
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
|
||||
|
||||
@@ -90,15 +474,23 @@ static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
}
|
||||
|
||||
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
const auto last_error = GetLastError();
|
||||
|
||||
// ignore signal if disabled or no exception info provided
|
||||
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) {
|
||||
if (!launcher::signal::DISABLE &&
|
||||
ExceptionInfo != nullptr &&
|
||||
ExceptionInfo->ExceptionRecord != nullptr &&
|
||||
InterlockedCompareExchange(&EXCEPTION_HANDLER_ACTIVE, 1, 0) == 0) {
|
||||
|
||||
// get exception record
|
||||
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
|
||||
|
||||
// print signal
|
||||
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
|
||||
log_exception_context(ExceptionInfo, last_error);
|
||||
|
||||
// write the minidump before other diagnostics inspect potentially damaged state
|
||||
write_minidump(ExceptionInfo);
|
||||
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
@@ -128,12 +520,11 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
// (e.g., ACIO init hang due to RTSS)
|
||||
deferredlogs::dump_to_logger(true);
|
||||
|
||||
// walk the exception chain
|
||||
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord;
|
||||
while (record_cause != nullptr) {
|
||||
log_warning("signal", "caused by: {}", exception_code(record_cause));
|
||||
record_cause = record_cause->ExceptionRecord;
|
||||
}
|
||||
// dump memory information before inspecting potentially damaged exception and stack data
|
||||
memutils::show_available_memory();
|
||||
|
||||
// walk the exception chain with bounded, checked reads
|
||||
log_exception_chain(ExceptionRecord);
|
||||
|
||||
// print stacktrace
|
||||
StackWalker sw;
|
||||
@@ -142,47 +533,12 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
log_warning("signal", "failed to print callstack");
|
||||
}
|
||||
|
||||
if (MiniDumpWriteDump_local != nullptr) {
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
CloseHandle(minidump_file);
|
||||
} else {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
}
|
||||
|
||||
// dump memory information
|
||||
memutils::show_available_memory();
|
||||
|
||||
// this will stall all UI threads for this process
|
||||
show_popup_for_crash();
|
||||
|
||||
log_fatal("signal", "end");
|
||||
|
||||
InterlockedExchange(&EXCEPTION_HANDLER_ACTIVE, 0);
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
@@ -4,18 +4,43 @@
|
||||
|
||||
#include "wintouchemu.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#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<TouchEvent> TOUCH_EVENTS;
|
||||
std::vector<TouchPoint> TOUCH_POINTS;
|
||||
HMODULE HOOKED_MODULE = nullptr;
|
||||
std::string WINDOW_TITLE_START = "";
|
||||
bool INITIALIZED = false;
|
||||
std::optional<std::string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<decltype(InitializeTouchInjection_ptr)>(
|
||||
user32, "InitializeTouchInjection");
|
||||
if (InitializeTouchInjection_ptr == nullptr) {
|
||||
log_warning(
|
||||
"touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user