Compare commits

...

24 Commits

Author SHA1 Message Date
bicarus 74e619df37 sdvx: fix check for enable_console in avs_config.xml (#838)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #657 

## Description of change
Workaround for SDVX4 was applied too broadly and caused sdvx 1/2/3 to
not boot, depending on contents of avs-config.xml.

`property_search_safe` throws a fatal error if the node is not present.
What we wanted to do was to check for presence.

## Testing
sanity checked sdvx 1/2/3/4
2026-07-26 16:16:25 -07:00
bicarus 10a97b9c63 signal: dump exception context, log dump creation failure (#837)
More diagnostics info for game crashes.
2026-07-26 15:25:24 -07:00
bicarus 159043803c imgui: fix crash when launching from UNC path (network shares) (#836)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
ImGui filebrowser extension crashes due to MinGW quirk about UNC path
handling.

## Testing
2026-07-26 04:30:06 -07:00
bicarus bd2fbfcb67 touch: restore wintouchemu, fall back to it when native touch hooks fail (#834)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #833

## Description of change
Last couple PRs - such as #820 #827 #828 - made the native touch hook &
touch injection using `InjectTouchInput` the default path, since it
performs much more reliably with both real touch screens and mouse (or
any other synthetic source).

However, user has reported that WINE lacks `InjectTouchInput` which
means this won't work.

As a fix, revive the old wintouchemu code. Native touch is still the
default, but under following circumstances:

1. if `-touchemuforce` is set, or
2. if any of the required Windows touch APIs are unavailable

then we fail over from native touch to wintouchemu code. 

For Linux, condition #2 would be hit during init, and gracefully switch
over.

Caveat: the poke code for IIDX and Nost will continue to require native
touch, I do not want to maintain two paths for this. This means that
iidx poke will stop working on Linux, unfortunately.

## Testing
Tested on Windows with `-touchemuforce` set. This is mostly reverting
Linux code path back to where we were last release, so this should just
work with wine.
2026-07-26 04:29:45 -07:00
bicarus 64600826b7 nost: touch piano mode (#830)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Touch piano mode. Allows piano keys to be played with the touch screen.
Since the touch screen is still needed for menu navigation (including
during songs) a toggle switch is added so that the user can flip between
navigation mode and piano mode.

The touch targets are pixel-perfect. Don't let the game's piano
animations fool you.

## Testing
2026-07-23 15:49:50 -07:00
bicarus 80deb4bbbd iidx: hide tdj sub screen in fullscreen single monitor mode (#829)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Prevent the game from launching a blank window when launching fullscreen
with sub disabled.

## Testing
2026-07-22 14:37:29 -07:00
bicarus 00c8bb8ce6 gitadora: use native touch for single-window mode (#828)
## Link to GitHub Issue or related Pull Request, if one exists
#814 

## Description of change
Make gitadora arena model use native touch instead of wintouchemu. Only
single window mode with overlay is affected.

This removes the last consumer of `INJECT_MOUSE_AS_WM_TOUCH` so all of
that code can be deleted.

## Testing
Tested touch screen and mouse interaction with single window mode.
2026-07-22 01:36:06 -07:00
bicarus 09ed8b948c 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.
2026-07-22 00:56:03 -07:00
bicarus e2daefbb6a overlay: can't press enter to select sub-tab in Controller tab (#826)
## Link to GitHub Issue or related Pull Request, if one exists
Another bug reported by a sea creature

## Description of change
Fix keyboard navigation in Controllers sub tabs.

## Testing
2026-07-21 21:13:18 -07:00
bicarus 39bf17aafe patcher: fix plus minus buttons for number patches (#825)
## Link to GitHub Issue or related Pull Request, if one exists
Reported by a certain sea reptile

## Description of change
Fix the + / - buttons for number patches not working. Regressed by one
of the ImGui version updates.

## Testing
seems fine now
2026-07-21 17:46:18 -07:00
bicarus 4836cd94ab 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
2026-07-21 16:57:11 -07:00
bicarus 9d41ca2515 nost: xact audio hook (#823)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #822

## Description of change
Add hooks for `libxact.dll` which only Nostalgia uses for audio. Detect
cases where the game would fail to launch (wrong number of channels) and
log a warning / deferred log.

## Testing
tested 2024102200
2026-07-21 15:33:30 -07:00
bicarus 18421ffbfe touch: make native touch the default for iidx/sdvx/popn (#820)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Remove iidx/sdvx/popon `native touch` options.

Remove dead code in wintouchemu that deals with these games.

Native touch is now at feature-parity with wintouchemu. Native touch
options work better with touch screens, while rawinput-based wintouchemu
has compat issues on some touchscreens. For these subscreen games
deprecate the usage of wintouchemu and make them use native touch stack
by default.

### For future consideration
Gitadora - not sure what to do with this one. We only use wintouchemu
for the single window case. For all other configurations, the game
accepts mouse and touch input without any hooks, so perhaps nothing
needs to be done here.

Nostalgia - consider moving away from wintouchemu and adopt native touch
hook here as well.

## Testing
WIP
2026-07-21 02:15:56 -07:00
bicarus 3f0921d983 gitadora: more windowed mode fixes (#819)
## Link to GitHub Issue or related Pull Request, if one exists
User reported

## Description of change

Sometimes the game moved the main window after `CreateWindowExA`
returned but before D3D initialization called
`graphics_capture_initial_window`. During this gap,
`GRAPHICS_HOOKED_WINDOW` was not set, so the placement hooks could not
identify the main window and preserve its monitor override. GITADORA
main, LEFT, and RIGHT windows are now registered immediately after
creation so later placement calls cannot undo their overrides.

This also adds `-gdwsmallsize` and `-gdwsmallpos` for explicitly setting
the SMALL window size and position. Explicit values override the
corresponding monitor-derived geometry, allowing the SMALL window's
10:16 aspect ratio to be preserved instead of stretching it to fill a
monitor.

## Testing
2026-07-21 00:54:09 -07:00
bicarus ed7318c270 rawinput: modifiers (key combinations) (#813)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #635

## Description of change
Adds optional **button modifiers**, allowing a binding to require one or
more "modifier" buttons to be held before it activates.

- New "Modifiers" controller page in the overlay config to bind the
Modifier 1–4 source buttons.
- Each binding gains a 4-bit modifier_mask (Modifier 1-4). It is
persisted as an optional modifiers XML attribute on button nodes and
controller-preset entries. The attribute is optional and defaults to 0,
so existing config files and presets load unchanged and the feature is
off unless the user opts in.
- The button Edit properties popup gains a "Modifiers" dropdown to pick
which modifiers a binding requires. Doesn't apply to MIDI though.
- Input evaluation skips a binding whose required modifiers are not
held, falling through to its alternatives; velocity reporting uses the
same gated path.
- Controller-preset templates gain a "Modifiers" group for importing and
exporting.

## Testing
2026-07-21 00:26:36 -07:00
bicarus 623e1e3998 touch: inject mouse and poke into native touch modes (#815)
## Link to GitHub Issue or related Pull Request, if one exists
Part 1 of many fixes for #814.

## Description of change
For the native touch hook - add code to inject synthetic touches using
Windows API (`InjectTouchInput`).

Note that this is Windows 8+ only, but we can make an assumption that we
are running on Win10+ for these games (TDJ/UFC/High Cheers) since the
cabs assume Win10.

Detect mouse events and allow IIDX poke to call into this to inject
synthetic touches.

Also, update the nativetouchhook to independently calculate window size
and rotation, instead of relying on rawinput layer.

## Testing
Tested to work with native touch option on IIDX, SDVX, POPN, all full
screen / windowed / sub on/off combinations.

Edge cases:

* sdvx main monitor rotated 270 deg
* touch invert option
* windowed mode resize / moved

All seem to work.
2026-07-21 00:04:38 -07:00
bicarus 558e3d0cb3 rawinput: avoid enumerationg adapter modes on first touch (#817)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #790

## Description of change
Avoid enumerating every display mode with `EnumDisplaySettings` during
the first raw touchscreen report, as it can take a couple hundred
milliseconds.

The latency-sensitive path now refreshes only the current display size
using `GetWindowRect(GetDesktopWindow())` and orientation using
`EnumDisplaySettingsEx(..., ENUM_CURRENT_SETTINGS, ..., EDS_RAWMODE)`.
Full `EnumDisplaySettings` enumeration runs only when native resolution
is needed for aspect compensation.

Jubeat and Reflec Beat now enable aspect compensation before
initializing touch input.

## Testing
Sanity checked rb/jb on laptops with non-16:9 ratio
2026-07-20 03:00:34 -07:00
bicarus d0db547dd0 sdvx: implement NVAPI for non-NVIDIA GPUs (#816)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

In summary: for SDVX VM mode, this PR eliminates the need for `Note FPS
Target 120Hz` / `Game FPS Target 120Hz` patches typically used for AMD
GPU via synthetic responses provided via a fake NVAPI layer.

Before this change, playing on AMD GPU required patches to force the
game to render at 60Hz (See #107 for relevant discussion). Basically, if
NVAPI query fails, the game shows

`I:NvDisplayConfig: GetMainDisplayRefreshRate = 60.000hz (DUMMY)`

the game renders at 120Hz but the notes render at 60Hz, which would look
stuttery.

After this change, even without a real `nvapi64.dll`, spice will provide
a stub implementation, just enough to convince the game that the main
display is 120Hz, which causes notes to render at 120Hz as well.

This can be overridden by `-graphics-force-refresh` option.

Important note: if user has the `Note FPS Target 60Hz` patch but running
the game at 120Hz, before this change the notes would render at 60Hz,
but after this they will be at 120Hz. I don't know why anyone would
intentionally lower the notes framerate, so this is not going to be
fixed. If the user wants to run the game at 60Hz they can do so via
`-graphics-force-refresh`.

Another change - `-nonvapi` will always force down this synthetic nvapi
path, even if the system has the real `nvapi64.dll`. Even on NVDIA GPU
this option can be used to force this.

## Testing
2026-07-19 23:43:14 -07:00
bicarus be4bb00390 patcher: patch groups (#812)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #245

## Description of change

Introduce patch groups. JSON schema now allows for a `"type": "group"`:

```json
    {
        "type": "group",
        "gameCode": "LDJ",
        "id": "timer-freeze",
        "name": "Timer Freeze Patches",
        "description": "Freezes various timers in the game."
    },
    {
        "name": "Standard/Menu Timer Freeze",
        "description": "Freezes all non-premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9962743,
                "dllName": "bm2dx.dll",
                "dataDisabled": "0F84",
                "dataEnabled": "90E9"
            }
        ]
    },
    {
        "name": "Premium Free Timer Freeze",
        "description": "Freezes all premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9111965,
                "dllName": "bm2dx.dll",
                "dataDisabled": "7E",
                "dataEnabled": "EB"
            }
        ]
    },
```

In the UI, these will show up as tree nodes. The parent is not actually
treated as a patch; e.g., its state is not saved to the patch manager
config file; only the child patches states are managed, the parent's
state only bubble up only in the UI.

In downlevel versions of spice, group parents will show up as a patch of
invalid type. Child patches will still show up as individual patches.

## Testing
*how was the code tested?*
2026-07-19 02:10:13 -07:00
bicarus a8cc85531c overlay: disable subscreen overlay if "native touch" option is on (#811)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
If IIDX/SDVX/POPN "native touch" option is enabled, show an error
message if user toggles subscreen window.

Those options are meant for users with touch screens, and disables mouse
interactions. Users who like to randomly turn on options often latch on
to this option and get confused when clicks don't register.

## Testing
2026-07-18 21:49:50 -07:00
bicarus 2dea0d27bf rawinput: fix hotplug for keyboard and mouse (#810)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long-standing issue of hotplug not working for keyboards and mice.
The code only registered for HID notifications; we also need to do it
for the keyboard GUID and mouse GUID.

## Testing
Confirmed that keyboards and mice can be hot unplugged and plugged.
2026-07-18 17:38:59 -07:00
bicarus 61a0220c1a audio: retain WASAPI clients only after initialization (#807)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #782 

## Description of change

This addresses an issue only seen on Linux + WINE.

Defer retaining WASAPI clients until `Initialize` succeeds.

Before this PR, temporary clients created during DirectShow device
capability probing were retained as active audio clients, causing
Wine/Linux crashes in DDR and popn.

Also covers IAudioClient3, dummy, ASIO, WaveOut, and null-device
initialization paths, for consistency.

## Testing
WIP
2026-07-18 03:09:37 -07:00
bicarus c080bbe301 rawinput: avoid deadlock when binding analog axis as a button (#809)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #793

## Description of change
Due to lock inversion, when binding an analog axis as a button, spice
deadlocks. Fix that.

Also create a separate `unordered_map` that keeps track of device
handles so that `WM_INPUT` handle can look up devices without having to
acquire the larger `devices_mutex` which could be held by (potentially)
lengthy operations like hotplug.

Fix more synchronization issues around hotplug. Latent bug exposed by
MIDI 2.0 issues.

## Testing
2026-07-18 03:09:27 -07:00
bicarus e1d1b39567 patcher: refactor (#808)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refactor patch manager.

This one file had the UI logic, config save/load, parsing patches JSON,
applying memory patches, everything all in one cpp file. Refactor and
separate out the layers.

## Testing
Sanity checked patch workflows.
2026-07-17 03:48:01 -07:00
91 changed files with 8141 additions and 3692 deletions
+17 -1
View File
@@ -417,6 +417,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/nost/nost.cpp
games/nost/io.cpp
games/nost/poke.cpp
games/nost/touch_mode.cpp
games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp
@@ -502,6 +503,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/audio/buffer.cpp
hooks/audio/mme.cpp
hooks/audio/util.cpp
hooks/audio/xact.cpp
hooks/audio/backends/dsound/dsound_backend.cpp
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
hooks/audio/backends/mmdevice/device.cpp
@@ -528,6 +530,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp
hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp
hooks/graphics/nvapi_impl.cpp
hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp
@@ -571,7 +574,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
misc/nativetouchhook.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
@@ -609,17 +611,26 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp
overlay/windows/log.cpp
overlay/windows/midi.cpp
overlay/windows/nostalgia_touch_piano.cpp
overlay/windows/obs.cpp
overlay/windows/obs_websocket.cpp
overlay/windows/patch_manager.cpp
overlay/windows/popn_sub.cpp
overlay/windows/wnd_manager.cpp
# patcher
patcher/config.cpp
patcher/lifecycle.cpp
patcher/loader.cpp
patcher/loader_group.cpp
patcher/runtime.cpp
# external
external/easywsclient/easywsclient.cpp
# rawinput
rawinput/rawinput.cpp
rawinput/rawinput_handles.cpp
rawinput/midi.cpp
rawinput/sextet.cpp
rawinput/piuio.cpp
@@ -648,6 +659,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
# touch
touch/gdi_overlay.cpp
touch/native/inject.cpp
touch/native/inject_mouse.cpp
touch/native/inject_synthetic.cpp
touch/native/nativetouchhook.cpp
touch/native/transform.cpp
touch/touch.cpp
touch/touch_gestures.cpp
touch/win7.cpp
+3 -1
View File
@@ -4,6 +4,7 @@
#include "launcher/launcher.h"
#include "misc/wintouchemu.h"
#include "rawinput/rawinput.h"
#include "touch/native/nativetouchhook.h"
// static stuff
static int ACIO_WARMUP = 0;
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
// flush device output
RI_MGR->devices_flush_output();
// update wintouchemu
// update touch emulation
wintouchemu::update();
nativetouch::refresh_contact_lifetime();
return 1;
}
+12
View File
@@ -3,6 +3,7 @@
#include "rawinput/rawinput.h"
#include "games/nost/io.h"
#include "games/nost/nost.h"
#include "games/nost/touch_mode.h"
#include "util/logging.h"
#include "avs/game.h"
@@ -16,6 +17,7 @@ using namespace GameAPI;
// static stuff
static uint8_t STATUS_BUFFER[277];
static bool STATUS_BUFFER_FREEZE = false;
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
/*
* Implementations
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
};
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
// iterate pairs of keys
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button0 > 0) {
state0 = button0;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
state0 < NOST_TOUCH_PIANO_VELOCITY) {
state0 = NOST_TOUCH_PIANO_VELOCITY;
}
const auto button1 = panb_get_button_velocity(
buttons.at(button_mapping[key_pair * 2 + 1]),
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button1 > 0) {
state1 = button1;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
state1 < NOST_TOUCH_PIANO_VELOCITY) {
state1 = NOST_TOUCH_PIANO_VELOCITY;
}
// build value
uint8_t value = 0;
+84 -2
View File
@@ -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,7 +141,11 @@ namespace api::modules {
}
// apply touch points
touch_write_points(&touch_points);
if (use_native) {
write_native(touch_points);
} else {
touch_write_points(&touch_points);
}
}
/**
@@ -123,7 +169,43 @@ namespace api::modules {
}
// remove all IDs
touch_remove_points(&touch_point_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) {
+14
View File
@@ -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);
};
+1 -1
View File
@@ -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");
}
+66 -11
View File
@@ -3,7 +3,9 @@
#include <cassert>
#include <optional>
#include "games/io.h"
#include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "rawinput/piuio.h"
#include "util/time.h"
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
return sorted;
}
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
namespace GameAPI::Buttons {
static State get_button_state(
rawinput::RawInputManager *manager,
Button &button,
bool check_alts,
bool check_modifiers);
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
}
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
const auto modifier_mask = button.getModifierMask();
if (modifier_mask == 0) {
return true;
}
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
if (!modifier_buttons) {
return false;
}
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
(index >= modifier_buttons->size() ||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
GameAPI::Buttons::BUTTON_PRESSED)) {
return false;
}
}
return true;
}
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
rawinput::RawInputManager *manager,
Button &_button,
bool check_alts,
bool check_modifiers) {
// check override
if (_button.override_enabled) {
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
std::optional<bool> window_has_focus;
while (true) {
// skip bindings whose required modifiers are not held
//
// note that modifiers cannot process MIDI as the logic is written
// below, since MIDI buttons are event-based (on event, off event, etc)
// and cannot be correctly handled by a simple early return
// there is no explicit check for MIDI here, but the UI should have
// prevented it
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
button_count++;
if (!alternatives || alternatives->empty() ||
button_count - 1 >= alternatives->size()) {
return BUTTON_NOT_PRESSED;
}
current_button = &alternatives->at(button_count - 1);
continue;
}
// naive behavior
if (current_button->isNaive()) {
GameAPI::Buttons::State state;
@@ -469,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
}
}
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
return get_button_state(manager, button, check_alts, true);
}
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
if (manager) {
return getState(manager.get(), button, check_alts);
@@ -484,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
return button.override_velocity;
}
// naive behavior
if (button.isNaive()) {
if (button.getInvert()) {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
} else {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
}
}
// get button state
Buttons::State button_state = Buttons::getState(manager, button, false);
const auto button_state = Buttons::getState(manager, button, false);
// naive bindings report their digital state as full or zero velocity
if (button.isNaive()) {
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
}
// check if button isn't being pressed
if (button_state != Buttons::BUTTON_PRESSED) {
+21 -1
View File
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
}
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
auto binding_display = this->getBindingDisplayString(manager);
auto modifiers = this->getModifierMask();
if (binding_display.empty() || modifiers == 0) {
return binding_display;
}
std::string modifier_display;
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
if ((modifiers & UINT8_C(1)) == 0) {
continue;
}
if (!modifier_display.empty()) {
modifier_display += "+";
}
modifier_display += fmt::format("Mod{}", index);
}
return modifier_display + "+" + binding_display;
}
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
// get VKey string
auto vKey = (uint16_t) this->getVKey();
+11
View File
@@ -49,6 +49,7 @@ private:
std::string name;
std::string device_identifier = "";
unsigned short vKey = INVALID_VKEY;
uint8_t modifier_mask = 0;
// default bindings are always naive
unsigned short vKey_default = INVALID_VKEY;
@@ -65,6 +66,7 @@ private:
unsigned short velocity_threshold = 0;
std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public:
std::string getVKeyString();
@@ -111,6 +113,7 @@ public:
device_identifier = "";
analog_type = BAT_NONE;
bat_threshold = 0;
modifier_mask = 0;
}
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -123,6 +126,14 @@ public:
return this->name;
}
inline uint8_t getModifierMask() const {
return this->modifier_mask;
}
inline void setModifierMask(uint8_t new_modifier_mask) {
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
}
inline const std::string &getDeviceIdentifier() const {
return this->device_identifier;
}
+12
View File
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
const char *devid = gameButtonNode->Attribute("devid");
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
gameButtonsNode->DeleteChild(gameButtonNode);
gameButtonNode = this->configFile.NewElement("button");
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert);
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
gameButtonsNode->InsertEndChild(gameButtonNode);
} else {
button->setVKey(static_cast<unsigned short int>(vKey));
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button->setDeviceIdentifier(devid);
}
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break;
}
}
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode);
}
}
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
const char *devid = gameButtonNode->Attribute("devid");
// find alternative
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert);
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
alt.setDeviceIdentifier(std::string(devid));
}
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button.setDeviceIdentifier(devid);
}
+46
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
el->SetAttribute("modifiers", entry.modifier_mask);
parent->InsertEndChild(el);
}
@@ -54,6 +55,10 @@ namespace overlay::windows {
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
unsigned int modifier_mask = 0;
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
return entry;
}
@@ -160,6 +165,28 @@ namespace overlay::windows {
}
}
// modifier buttons
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
if (mod_el) {
auto *btn_el = mod_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.modifier_buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.modifier_buttons.push_back(
{btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// keypad buttons
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
if (kp_el) {
@@ -244,6 +271,7 @@ namespace overlay::windows {
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights) {
@@ -300,6 +328,24 @@ namespace overlay::windows {
}
tmpl_el->InsertEndChild(buttons_el);
// modifier buttons - skip unbound entries
if (!tmpl.modifier_buttons.empty()) {
auto *mod_el = doc.NewElement("modifier_buttons");
if (save_modifiers) {
for (auto &btn : tmpl.modifier_buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(mod_el);
}
// keypad buttons — skip unbound entries
if (!tmpl.keypad_buttons.empty()) {
auto *kp_el = doc.NewElement("keypad_buttons");
+9 -1
View File
@@ -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';
-1
View File
@@ -7,7 +7,6 @@
#include "util/utils.h"
#include "util/detour.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2x_hook.h"
#include "trackball.h"
+8 -3
View File
@@ -13,6 +13,7 @@
#include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h"
@@ -653,9 +654,13 @@ namespace games::gitadora {
// monitor/touch hooks (windowed or full screen)
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", 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
+18 -7
View File
@@ -20,9 +20,9 @@
#include "hooks/sleephook.h"
#include "launcher/options.h"
#include "touch/touch.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.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"
@@ -41,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;
@@ -63,7 +64,8 @@ namespace games::iidx {
bool TDJ_MODE = false;
bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = 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;
@@ -350,12 +352,15 @@ namespace games::iidx {
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE);
if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else {
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);
wintouchemu::hook_title_ends(
"beatmania IIDX",
"main",
avs::game::DLL_INSTANCE);
}
}
@@ -533,6 +538,12 @@ namespace games::iidx {
}
}
void IIDXGame::post_attach() {
if (ENABLE_POKE) {
poke::enable();
}
}
void IIDXGame::detach() {
Game::detach();
+2
View File
@@ -29,6 +29,7 @@ namespace games::iidx {
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;
@@ -53,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:
+24 -143
View File
@@ -3,30 +3,16 @@
#include <thread>
#include "windows.h"
#include "cfg/screen_resize.h"
#include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h"
#include "touch/native/inject.h"
#include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#define POKE_NATIVE_TOUCH 0
#if POKE_NATIVE_TOUCH
static HINSTANCE USER32_INSTANCE = nullptr;
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
static InjectTouchInput_t pInjectTouchInput = nullptr;
#endif
namespace games::iidx::poke {
static std::thread *THREAD = nullptr;
@@ -116,88 +102,26 @@ namespace games::iidx::poke {
{"1/D", 50},
};
#if POKE_NATIVE_TOUCH
void initialize_native_touch_library() {
if (USER32_INSTANCE == nullptr) {
USER32_INSTANCE = libutils::load_library("user32.dll");
}
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
USER32_INSTANCE, "InitializeTouchInjection");
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
USER32_INSTANCE, "InjectTouchInput");
}
void emulate_native_touch(TouchPoint tp, bool is_down) {
if (pInjectTouchInput == nullptr) {
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
if (touch_points.empty()) {
return;
}
POINTER_TOUCH_INFO contact;
BOOL bRet = TRUE;
const int contact_offset = 2;
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.ptPixelLocation.x = tp.x;
contact.pointerInfo.ptPixelLocation.y = tp.y;
if (is_down) {
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
} else {
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
}
contact.pointerInfo.dwTime = 0;
contact.pointerInfo.PerformanceCount = 0;
contact.touchFlags = TOUCH_FLAG_NONE;
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
contact.orientation = 0;
contact.pressure = 1024;
contact.rcContact.top = tp.y - contact_offset;
contact.rcContact.bottom = tp.y + contact_offset;
contact.rcContact.left = tp.x - contact_offset;
contact.rcContact.right = tp.x + contact_offset;
bRet = InjectTouchInput(1, &contact);
if (!bRet) {
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
}
const auto &touch = touch_points.front();
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
}
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
int i = 0;
for (auto &touch : *touch_points) {
emulate_native_touch(touch, true);
}
}
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
for (auto &touch : *touch_points) {
emulate_native_touch(touch, false);
}
touch_points->clear();
}
#endif
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
}
void enable() {
// check if already running
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([] {
@@ -210,35 +134,13 @@ namespace games::iidx::poke {
std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0};
#if POKE_NATIVE_TOUCH
bool use_native = games::iidx::NATIVE_TOUCH;
if (use_native) {
initialize_native_touch_library();
if (pInitializeTouchInjection != nullptr) {
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
}
}
#else
bool use_native = false;
#endif
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
clear_native_touch_points(&touch_points);
} else {
clear_touch_points(&touch_points);
}
#else
clear_touch_points(&touch_points);
#endif
inject_native_touch_points(touch_points, false);
touch_points.clear();
}
for (int unit = 0; unit < 2; unit++) {
@@ -263,32 +165,15 @@ namespace games::iidx::poke {
float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0;
if (use_native) {
x *= rawinput::TOUCHSCREEN_RANGE_X;
y *= rawinput::TOUCHSCREEN_RANGE_Y;
} else if (GRAPHICS_IIDX_WSUB) {
// Scale to windowed subscreen
x *= GRAPHICS_WSUB_WIDTH;
y *= GRAPHICS_WSUB_HEIGHT;
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
// Overlay is not present, scale to main screen
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH;
y *= SPICETOUCH_TOUCH_HEIGHT;
} else {
x *= ImGui::GetIO().DisplaySize.x;
y *= ImGui::GetIO().DisplaySize.y;
if (GRAPHICS_WINDOWED) {
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
continue;
}
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
} else {
// Overlay subscreen
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
// Scale to window size ratio
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
}
x = x_iter->second;
y = y_iter->second;
}
TouchPoint tp {
@@ -309,21 +194,17 @@ namespace games::iidx::poke {
} // for all units
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
emulate_native_touch_points(&touch_points);
} else {
touch_write_points(&touch_points);
}
#else
touch_write_points(&touch_points);
#endif
inject_native_touch_points(touch_points, true);
}
// slow down
timer.sleep(50);
}
if (!touch_points.empty()) {
inject_native_touch_points(touch_points, false);
}
return nullptr;
});
}
+32
View File
@@ -44,6 +44,7 @@ namespace games {
static bool IO_INITIALIZED = false;
static std::vector<std::string> games;
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
static robin_hood::unordered_map<std::string, std::string> buttons_help;
@@ -365,6 +366,37 @@ namespace games {
return it->second;
}
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
const std::vector<std::string> names {
"Modifier 1",
"Modifier 2",
"Modifier 3",
"Modifier 4",
};
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
for (auto &modifier : modifier_buttons) {
modifier.setModifierMask(0);
for (auto &alternative : modifier.getAlternatives()) {
alternative.setModifierMask(0);
}
}
return modifier_buttons;
}
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
initialize();
auto it = buttons_modifiers.find(game);
if (it == buttons_modifiers.end()) {
if (game.empty()) {
return nullptr;
}
buttons_modifiers[game] = gen_buttons_modifiers(game);
return &buttons_modifiers[game];
}
return &it->second;
}
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
auto buttons = GameAPI::Buttons::getButtons(game);
std::vector<std::string> names;
+11
View File
@@ -5,6 +5,16 @@
namespace games {
namespace ModifierButtons {
enum {
Modifier1,
Modifier2,
Modifier3,
Modifier4,
Size,
};
}
namespace OverlayButtons {
enum {
Screenshot,
@@ -59,6 +69,7 @@ namespace games {
const std::vector<std::string> &get_games();
std::vector<Button> *get_buttons(const std::string &game);
std::vector<Button> *get_buttons_modifiers(const std::string &game);
std::string get_buttons_help(const std::string &game);
std::string get_analogs_help(const std::string &game);
std::vector<Button> *get_buttons_keypads(const std::string &game);
+2 -1
View File
@@ -202,11 +202,12 @@ namespace games::jb {
IS_PORTRAIT = avs::game::is_model("L44");
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
touch_create_wnd(wnd, true);
// let the rawinput stack correct the aspect ratio
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
touch_create_wnd(wnd, true);
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
-1
View File
@@ -6,7 +6,6 @@
#include "util/utils.h"
#include "util/execexe.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2a_hook.h"
+16 -7
View File
@@ -1,16 +1,15 @@
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "nost.h"
#include "poke.h"
#include "touch_mode.h"
#include "hooks/setupapihook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "avs/game.h"
namespace games::nost {
bool ENABLE_POKE = false;
bool ENABLE_TOUCH_MODE = false;
NostGame::NostGame() : Game("Nostalgia") {
}
@@ -43,9 +42,16 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings);
// custom touch
// nostalgia crashes if you inject touch events too early
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
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();
}
}
}
void NostGame::post_attach() {
@@ -55,6 +61,9 @@ namespace games::nost {
}
void NostGame::detach() {
if (ENABLE_TOUCH_MODE) {
touch_mode::disable();
}
if (ENABLE_POKE) {
poke::disable();
}
+1
View File
@@ -5,6 +5,7 @@
namespace games::nost {
extern bool ENABLE_POKE;
extern bool ENABLE_TOUCH_MODE;
class NostGame : public games::Game {
public:
+94 -76
View File
@@ -1,22 +1,25 @@
#include "poke.h"
#include <thread>
#include <atomic>
#include <optional>
#include <thread>
#include "games/nost/io.h"
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "misc/eamuse.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke {
static std::thread *THREAD = nullptr;
static volatile bool THREAD_RUNNING = false;
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
static std::atomic<bool> THREAD_RUNNING { false };
// 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_1, {760, 385}},
{EAM_IO_KEYPAD_2, {820, 385}},
@@ -31,7 +34,8 @@ namespace games::nost::poke {
{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::PokeBack, {340, 100}},
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
@@ -46,13 +50,8 @@ namespace games::nost::poke {
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
};
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
static bool inject_poke_position(POINT position, bool down) {
return nativetouch::inject::inject_synthetic_touch(position, down);
}
void enable() {
@@ -61,128 +60,147 @@ 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([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300;
const int swipe_next_page_x_begin = 1120;
const int swipe_next_page_x_end = 1120-120;
const int swipe_next_page_x_end = 1120 - 120;
const int swipe_prev_page_x_begin = 280-120;
const int swipe_prev_page_x_begin = 280 - 120;
const int swipe_prev_page_x_end = 280;
int next_page_anim_index = -1;
int prev_page_anim_index = -1;
std::vector<TouchPoint> touch_points;
bool touch_release = false;
bool contact_active = false;
bool release_pending = false;
POINT last_position {};
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
uint16_t last_keypad_state = 0;
// log
log_info("poke", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (!touch_points.empty()) {
if (touch_release) {
clear_touch_points(&touch_points);
touch_release = false;
} else {
touch_points.clear();
if (release_pending) {
if (contact_active) {
inject_poke_position(last_position, false);
}
contact_active = false;
release_pending = false;
}
auto &buttons = games::nost::get_buttons();
std::optional<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) {
const auto delta =
(swipe_next_page_x_end - swipe_next_page_x_begin)
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_next_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_next_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
next_page_anim_index--;
if (next_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else if (0 <= prev_page_anim_index) {
const auto delta =
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_prev_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_prev_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
prev_page_anim_index--;
if (prev_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else {
for (const auto& it : NOST_POKE) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
if (!touch_release) {
const auto state = eamuse_get_keypad_state(0);
if (state) {
for (const auto& it : NOST_POKE_NUM) {
if (state & (1 << it.first)) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
}
}
touch_position = triggered_touch_position;
release_after_touch = touch_position.has_value();
// start animations for next frame
if (!touch_release) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
if (!touch_position.has_value()) {
if (next_triggered) {
next_page_anim_index = swipe_anim_total_frames;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
if (prev_triggered) {
prev_page_anim_index = swipe_anim_total_frames;
}
}
}
if (!touch_points.empty()) {
touch_write_points(&touch_points);
if (touch_position.has_value() &&
inject_poke_position(*touch_position, true)) {
contact_active = true;
last_position = *touch_position;
}
if (release_after_touch && contact_active) {
release_pending = true;
}
// slow down
timer.sleep(30);
}
if (contact_active) {
inject_poke_position(last_position, false);
}
return nullptr;
});
}
+233
View File
@@ -0,0 +1,233 @@
#include "touch_mode.h"
#include <atomic>
#include <mutex>
#include <unordered_map>
#include "touch/native/nativetouchhook.h"
#include "util/logging.h"
namespace games::nost::touch_mode {
// native contact positions feed piano input directly. native events reach the game
// in nav mode and are suppressed in piano mode. mode-button contacts are always
// suppressed and change that routing after release.
static constexpr LONG PIANO_LEFT_GAP = 11;
static constexpr LONG PIANO_RIGHT_GAP = 10;
static constexpr uint32_t PIANO_KEY_COUNT = 28;
struct TouchGeometry {
HWND window = nullptr;
RECT mode_button {};
LONG client_width = 0;
LONG client_height = 0;
bool valid() const {
return window != nullptr && client_width > 0 && client_height > 0;
}
};
struct NativeContact {
POINT position {};
// position contains game-client coordinates
bool client_position_valid = false;
// down began on the mode switch button; remains true through up
bool mode_button = false;
};
static std::atomic_bool accept_events { false };
static std::atomic<Mode> current_mode_state { Mode::Nav };
static std::mutex state_mutex;
static TouchGeometry touch_geometry;
// native contacts are kept by ID so each contact contributes exactly one position
static std::unordered_map<DWORD, NativeContact> active_contacts;
// a hardware button release requests one change after all contacts are released
static bool mode_change_pending = false;
static void reset_state_locked() {
current_mode_state.store(Mode::Nav, std::memory_order_release);
touch_geometry = {};
active_contacts.clear();
mode_change_pending = false;
}
// hardware contacts arrive in screen coordinates
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
if (!touch_geometry.valid()) {
return false;
}
POINT position { event.x, event.y };
if (!ScreenToClient(touch_geometry.window, &position)) {
return false;
}
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
}
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
std::lock_guard<std::mutex> lock(state_mutex);
// first, process down / move events
if (event.down || event.move) {
auto contact = active_contacts.try_emplace(event.id).first;
// keep track of IDs that began as a down on the mode switch button
if (event.down) {
contact->second.mode_button = native_touch_in_button(event);
}
// check for valid position
POINT position { event.x, event.y };
if (touch_geometry.window != nullptr &&
ScreenToClient(touch_geometry.window, &position)) {
contact->second.position = position;
contact->second.client_position_valid = true;
}
}
const auto contact = active_contacts.find(event.id);
const bool mode_button_contact = contact != active_contacts.end() &&
contact->second.mode_button;
// process up events
if (event.up) {
active_contacts.erase(event.id);
// if a contact that began down event on the mode switch button has
// been released, a mode switch is now pending
if (mode_button_contact) {
mode_change_pending = true;
}
// apply the change on the final hardware up. switching earlier would
// split another contact's down and up events across different modes
if (mode_change_pending && active_contacts.empty()) {
mode_change_pending = false;
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
current_mode_state.store(next_mode, std::memory_order_release);
}
}
return mode_button_contact;
}
// install the Nostalgia-specific native touch interception
void enable() {
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
return;
}
{
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
nativetouch::set_input_filter(filter_native_touch);
log_info("nost::touch", "enabled");
}
void disable() {
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
return;
}
nativetouch::set_input_filter(nullptr);
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
bool enabled() {
return accept_events.load(std::memory_order_acquire);
}
Mode current_mode() {
return current_mode_state.load(std::memory_order_acquire);
}
// publish the rendered overlay button rectangle in game-client pixels
void publish_button_bounds(HWND window, const RECT &client_bounds) {
TouchGeometry next {};
RECT client_rect {};
if (window != nullptr && GetClientRect(window, &client_rect) &&
client_rect.right > 0 && client_rect.bottom > 0) {
next.window = window;
next.mode_button = client_bounds;
next.client_width = client_rect.right;
next.client_height = client_rect.bottom;
}
std::lock_guard<std::mutex> lock(state_mutex);
touch_geometry = next;
}
// return the active 28-key piano bitfield for the PANB input update
uint32_t piano_key_state() {
if (!enabled() || current_mode() != Mode::Piano) {
return 0;
}
std::lock_guard<std::mutex> lock(state_mutex);
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
return 0;
}
uint32_t state = 0;
for (const auto &contact : active_contacts) {
// invalid position or mode-button contact; ignore these contacts
if (!contact.second.client_position_valid || contact.second.mode_button) {
continue;
}
const auto &position = contact.second.position;
// outside the client area or on the mode button; ignore these contacts
if (position.x < 0 || position.x >= touch_geometry.client_width ||
position.y < 0 || position.y >= touch_geometry.client_height ||
PtInRect(&touch_geometry.mode_button, position)) {
continue;
}
// divide the inset width evenly; touches in either side gap clamp to
// the nearest outer key so the physical screen edges remain playable
const auto piano_width =
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
uint32_t key = 0;
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
key = PIANO_KEY_COUNT - 1;
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
key = static_cast<uint32_t>(
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
}
state |= UINT32_C(1) << key;
}
return state;
}
// update native contacts and report whether this event should be hidden from the game
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
// synthetic events are outside this hardware-only feature
if (!enabled() || event.synthetic) {
// false leaves the event visible to the game
return false;
}
// snapshot routing before an up event can commit a pending mode switch
const bool piano_mode_before_update = current_mode() == Mode::Piano;
// update the contact lifetime and commit any pending switch when safe
const bool mode_button_contact = update_touch_state(event);
// hide every event in a contact that began on the mode switch button
if (mode_button_contact) {
return true;
}
// piano mode consumes hardware events; nav mode forwards them to the game
return piano_mode_before_update;
}
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <windows.h>
#include "touch/native/nativetouchhook.h"
namespace games::nost::touch_mode {
// nav mode forwards contacts to the game; piano mode converts them into piano keys
enum class Mode {
Nav,
Piano,
};
void enable();
void disable();
bool enabled();
Mode current_mode();
void publish_button_bounds(HWND window, const RECT &client_bounds);
uint32_t piano_key_state();
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
}
-1
View File
@@ -12,7 +12,6 @@
#include "util/unity_player.h"
#include "util/execexe.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/rawinput.h"
#include "util/socd_cleaner.h"
-1
View File
@@ -2,7 +2,6 @@
#include "acio/icca/icca.h"
#include "io.h"
#include "misc/wintouchemu.h"
#include "util/detour.h"
#include "util/utils.h"
#include "util/libutils.h"
+12 -9
View File
@@ -16,16 +16,16 @@
#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"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
namespace games::popn {
bool SHOW_PIKA_MONITOR_WARNING = false;
bool NATIVE_TOUCH = false;
bool NATIVE_TOUCH = true;
#if SPICE64 && !SPICE_XP
@@ -715,13 +715,16 @@ namespace games::popn {
// 00000100 0000000B 00000001 (button 9)
// set third column to 0 and it will work with BIO2
if (!GRAPHICS_WINDOWED) {
if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else {
wintouchemu::FORCE = true;
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);
wintouchemu::hook_title_ends(
"",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
+3 -3
View File
@@ -86,6 +86,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
// create touch window
touch_create_wnd(wnd);
@@ -94,9 +97,6 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
ShowWindow(wnd, SW_SHOW);
}
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else {
// fallback to dx hook
+25 -10
View File
@@ -8,6 +8,9 @@
#include "hooks/graphics/graphics.h"
#include "hooks/devicehook.h"
#include "hooks/libraryhook.h"
#ifdef SPICE64
#include "hooks/graphics/nvapi_impl.h"
#endif
#include "hooks/graphics/nvapi_hook.h"
#include "hooks/powrprof.h"
#include "hooks/sleephook.h"
@@ -23,8 +26,8 @@
#include "util/libutils.h"
#include "util/sysutils.h"
#include "misc/eamuse.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "bi2x_hook.h"
#include "camera.h"
#include "io.h"
@@ -46,7 +49,6 @@ namespace games::sdvx {
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
// settings
bool NATIVETOUCH = false;
uint8_t DIGITAL_KNOB_SENS = 16;
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
bool ENABLE_COM_PORT_SCAN_HOOK = false;
@@ -428,28 +430,41 @@ namespace games::sdvx {
if (aio != nullptr) {
// enable 9on12 for AMD
if (!libutils::try_library("nvapi64.dll")) {
const auto nvapi = libutils::try_library("nvapi64.dll");
if (nvapi == nullptr) {
log_info(
"sdvx",
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
}
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
if (!nvapi_impl::initialize(
avs::game::DLL_INSTANCE,
main_refresh_hz,
sub_refresh_hz)) {
log_warning("sdvx", "failed to initialize synthetic NVAPI");
}
} else {
// don't let nvapi mess with display settings
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
}
if (is_valkyrie_model()) {
if (NATIVETOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
// hook touch window
// in windowed mode, game can accept mouse input on the second screen
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_title_ends(
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
-1
View File
@@ -18,7 +18,6 @@ namespace games::sdvx {
};
// settings
extern bool NATIVETOUCH;
extern uint8_t DIGITAL_KNOB_SENS;
extern std::optional<std::string> ASIO_DRIVER;
extern SdvxOverlayPosition OVERLAY_POS;
+46 -5
View File
@@ -7,6 +7,7 @@
#include <audioclient.h>
#include <mmdeviceapi.h>
#include "avs/game.h"
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
#include "util/detour.h"
#include "util/logging.h"
@@ -15,6 +16,7 @@
#include "audio_private.h"
#include "acm.h"
#include "asio_proxy.h"
#include "xact.h"
#ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator,
@@ -53,9 +55,39 @@ namespace hooks::audio {
std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
// private globals
IAudioClient *CLIENT = nullptr;
std::mutex INITIALIZE_LOCK; // for asio
IAudioClient *CLIENT = nullptr;
static std::mutex CLIENT_LOCK;
static bool CLIENT_STOPPING = false;
void set_active_client(IAudioClient *client, const char *source) {
IAudioClient *previous_client = nullptr;
{
std::lock_guard lock(CLIENT_LOCK);
if (CLIENT_STOPPING || CLIENT == client) {
return;
}
if (client) {
client->AddRef();
}
previous_client = CLIENT;
CLIENT = client;
}
if (previous_client) {
previous_client->Release();
}
if (client) {
log_info(
"audio",
"active client selected by {} after successful initialization",
source);
}
}
}
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
@@ -129,15 +161,24 @@ namespace hooks::audio {
// general hooks
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
if (avs::game::is_model("PAN")) {
hooks::audio::xact::init();
}
}
void stop() {
log_info("audio", "stopping");
if (CLIENT) {
CLIENT->Stop();
CLIENT->Release();
IAudioClient *client = nullptr;
{
std::lock_guard lock(CLIENT_LOCK);
CLIENT_STOPPING = true;
client = CLIENT;
CLIENT = nullptr;
}
if (client) {
client->Stop();
client->Release();
}
stop_low_latency();
// release the ASIO drivers we kept pinned for the process lifetime now that we are
+2
View File
@@ -14,4 +14,6 @@ namespace hooks::audio {
extern std::mutex INITIALIZE_LOCK;
extern bool VOLUME_HOOK_ENABLED;
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
void set_active_client(IAudioClient *client, const char *source);
}
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
}
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
// release old audio client if initialized
if (hooks::audio::CLIENT) {
hooks::audio::CLIENT->Release();
}
IAudioClient *client = nullptr;
if (iid == IID_IAudioClient) {
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
}
*ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
if (iid == IID_IAudioClient) {
// release any previously persisted client
if (hooks::audio::CLIENT != nullptr) {
hooks::audio::CLIENT->Release();
}
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
*ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
return S_OK;
}
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
device_format->nChannels * (device_format->wBitsPerSample / 8));
}
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
return ret;
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
@@ -651,5 +652,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat);
copy_wave_format(&this->device_format, pFormat);
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
return ret;
}
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
CHECK_RESULT(this->backend->on_initialize(
HRESULT ret = this->backend->on_initialize(
&ShareMode,
&StreamFlags,
&hnsBufferDuration,
&hnsPeriodicity,
pFormat,
AudioSessionGuid));
AudioSessionGuid);
if (SUCCEEDED(ret)) {
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
}
CHECK_RESULT(ret);
}
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
static std::once_flag printed;
+393
View File
@@ -0,0 +1,393 @@
#include "xact.h"
#include <atomic>
#include <string>
#include <windows.h>
#include <initguid.h>
#include <mmreg.h>
#include <objbase.h>
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
namespace hooks::audio::xact {
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
// layout, so keep this proxy pinned to the legacy ABI used by libxact.
struct XAudio2DeviceDetails {
WCHAR device_id[256];
WCHAR display_name[256];
DWORD role;
WAVEFORMATEXTENSIBLE output_format;
};
struct XAudio2EffectChain {
UINT32 effect_count;
const void *effect_descriptors;
};
struct IXAudio2_27 {
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
virtual ULONG STDMETHODCALLTYPE Release() = 0;
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
UINT32 device_index,
XAudio2DeviceDetails *device_details) = 0;
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
void **source_voice,
const WAVEFORMATEX *source_format,
UINT32 flags,
float max_frequency_ratio,
void *callback,
const void *send_list,
const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
void **submix_voice,
UINT32 input_channels,
UINT32 input_sample_rate,
UINT32 flags,
UINT32 processing_stage,
const void *send_list,
const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
void **mastering_voice,
UINT32 input_channels,
UINT32 input_sample_rate,
UINT32 flags,
UINT32 device_index,
const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
virtual void STDMETHODCALLTYPE StopEngine() = 0;
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
virtual void STDMETHODCALLTYPE SetDebugConfiguration(
const void *debug_configuration,
void *reserved) = 0;
};
// XAudio2 2.7 COM class and interface.
DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
0x5a508685, 0xa254, 0x4fba,
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
DEFINE_GUID(IID_IXAudio2_7_LEGACY,
0x8bcf1f58, 0x9fe7, 0x4583,
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
static CreateFX_t CreateFX_orig = nullptr;
static std::string describe_wave_format(const WAVEFORMATEX *format) {
if (format == nullptr) {
return "null";
}
DWORD channel_mask = 0;
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
}
return fmt::format(
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
format->wFormatTag,
format->nChannels,
format->nSamplesPerSec,
format->wBitsPerSample,
format->nBlockAlign,
format->nAvgBytesPerSec,
channel_mask);
}
template <size_t Size>
static std::string narrow_fixed(const WCHAR (&value)[Size]) {
size_t length = 0;
while (length < Size && value[length] != L'\0') {
length++;
}
return ws2s(std::wstring(value, length));
}
class WrappedXAudio2 final : public IXAudio2_27 {
public:
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
if (object == nullptr) {
return E_POINTER;
}
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
*object = this;
AddRef();
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
return S_OK;
}
const auto result = real->QueryInterface(riid, object);
log_info(
"audio::xaudio2",
"IXAudio2::QueryInterface({}) -> {}, object={}",
guid2s(riid),
FMT_HRESULT(result),
object != nullptr ? *object : nullptr);
return result;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return ++ref_count;
}
ULONG STDMETHODCALLTYPE Release() override {
const auto remaining = --ref_count;
if (remaining == 0) {
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
real->Release();
delete this;
}
return remaining;
}
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
const auto result = real->GetDeviceCount(device_count);
log_info(
"audio::xaudio2",
"IXAudio2::GetDeviceCount -> {}, count={}",
FMT_HRESULT(result),
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
return result;
}
HRESULT STDMETHODCALLTYPE GetDeviceDetails(
UINT32 device_index,
XAudio2DeviceDetails *device_details) override {
const auto result = real->GetDeviceDetails(device_index, device_details);
if (SUCCEEDED(result) && device_details != nullptr) {
const auto device_name = narrow_fixed(device_details->display_name);
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
log_info(
"audio::xaudio2",
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
device_index,
FMT_HRESULT(result),
narrow_fixed(device_details->device_id),
device_name,
device_details->role,
describe_wave_format(&device_details->output_format.Format));
}
const auto channels = device_details->output_format.Format.nChannels;
if (channels != 2 && channels != 6 &&
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
log_warning(
"audio::xaudio2",
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
device_name,
channels);
deferredlogs::defer_error_messages({
"unsupported audio output channel count detected!",
fmt::format(" device: {}", device_name),
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
" * configure the default Windows playback device for stereo or 5.1 output",
" * disable 7.1 surround sound or spatial audio for this device",
});
}
} else {
log_warning(
"audio::xaudio2",
"IXAudio2::GetDeviceDetails({}) -> {}",
device_index,
FMT_HRESULT(result));
}
return result;
}
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
const auto result = real->Initialize(flags, processor);
log_info(
"audio::xaudio2",
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
flags,
processor,
FMT_HRESULT(result));
return result;
}
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
const auto result = real->RegisterForCallbacks(callback);
log_info(
"audio::xaudio2",
"IXAudio2::RegisterForCallbacks({}) -> {}",
callback,
FMT_HRESULT(result));
return result;
}
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
real->UnregisterForCallbacks(callback);
}
HRESULT STDMETHODCALLTYPE CreateSourceVoice(
void **source_voice,
const WAVEFORMATEX *source_format,
UINT32 flags,
float max_frequency_ratio,
void *callback,
const void *send_list,
const XAudio2EffectChain *effect_chain) override {
return real->CreateSourceVoice(
source_voice,
source_format,
flags,
max_frequency_ratio,
callback,
send_list,
effect_chain);
}
HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
void **submix_voice,
UINT32 input_channels,
UINT32 input_sample_rate,
UINT32 flags,
UINT32 processing_stage,
const void *send_list,
const XAudio2EffectChain *effect_chain) override {
return real->CreateSubmixVoice(
submix_voice,
input_channels,
input_sample_rate,
flags,
processing_stage,
send_list,
effect_chain);
}
HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
void **mastering_voice,
UINT32 input_channels,
UINT32 input_sample_rate,
UINT32 flags,
UINT32 device_index,
const XAudio2EffectChain *effect_chain) override {
const auto result = real->CreateMasteringVoice(
mastering_voice,
input_channels,
input_sample_rate,
flags,
device_index,
effect_chain);
log_info(
"audio::xaudio2",
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
input_channels,
input_sample_rate,
flags,
device_index,
effect_chain != nullptr ? effect_chain->effect_count : 0,
FMT_HRESULT(result),
mastering_voice != nullptr ? *mastering_voice : nullptr);
return result;
}
HRESULT STDMETHODCALLTYPE StartEngine() override {
const auto result = real->StartEngine();
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
return result;
}
void STDMETHODCALLTYPE StopEngine() override {
log_info("audio::xaudio2", "IXAudio2::StopEngine");
real->StopEngine();
}
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
return real->CommitChanges(operation_set);
}
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
real->GetPerformanceData(performance_data);
}
void STDMETHODCALLTYPE SetDebugConfiguration(
const void *debug_configuration,
void *reserved) override {
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
real->SetDebugConfiguration(debug_configuration, reserved);
}
private:
std::atomic<ULONG> ref_count = 1;
std::atomic_bool device_details_logged = false;
std::atomic_bool channel_warning_logged = false;
IXAudio2_27 *real;
};
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
REFCLSID clsid,
LPUNKNOWN outer,
DWORD class_context,
REFIID iid,
LPVOID *object) {
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
log_info(
"audio::xact",
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
guid2s(clsid),
guid2s(iid),
class_context,
FMT_HRESULT(result),
object != nullptr ? *object : nullptr);
if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
*object = static_cast<IXAudio2_27 *>(
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
}
return result;
}
static HRESULT WINAPI CreateFX_hook(
REFCLSID clsid,
IUnknown **effect,
const void *init_data,
UINT32 init_data_size) {
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
log_info(
"audio::xapofx",
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
guid2s(clsid),
init_data,
init_data_size,
FMT_HRESULT(result),
effect != nullptr ? static_cast<void *>(*effect) : nullptr);
return result;
}
void init() {
const auto libxact = GetModuleHandleW(L"libxact.dll");
if (libxact == nullptr) {
return;
}
CoCreateInstance_orig = detour::iat_try(
"CoCreateInstance", CoCreateInstance_hook, libxact);
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
log_info(
"audio::xact",
"libxact hooks installed: CoCreateInstance={}, CreateFX={}",
CoCreateInstance_orig != nullptr,
CreateFX_orig != nullptr);
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace hooks::audio::xact {
void init();
}
+31 -6
View File
@@ -29,6 +29,7 @@
#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"
@@ -188,11 +189,13 @@ static bool gitadora_should_block_game_window_placement(HWND hWnd) {
}
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
return window_name != nullptr && graphics_gitadora_has_window_override(window_name);
}
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
if (window_name == "LEFT") {
if (window_name == "GITADORA") {
GRAPHICS_HOOKED_WINDOW = hWnd;
} else if (window_name == "LEFT") {
GFDM_LEFT_WINDOW = hWnd;
} else if (window_name == "RIGHT") {
GFDM_RIGHT_WINDOW = hWnd;
@@ -589,6 +592,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result);
// remember these windows now because full capture happens later during D3D
// initialization; placement calls before then can undo creation-time overrides
if (result != nullptr && is_gfdm_window && !is_gfdm_sub_window) {
gitadora_remember_window(result, gfdm_window_name);
}
// theme the native title bar (dark/light)
set_window_dark_titlebar(result);
@@ -609,9 +618,6 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
// only hook touch window if multiple windows are allowed
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
gitadora_remember_window(result, gfdm_window_name);
}
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_remember_window(result, gfdm_window_name);
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
@@ -797,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;
touch_attach_wnd(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(
@@ -950,6 +960,13 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
return true;
}
if (games::iidx::TDJ_MODE &&
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd == TDJ_SUBSCREEN_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true;
}
// call original
return ShowWindow_orig(hWnd, nCmdShow);
}
@@ -1163,6 +1180,14 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
const bool native_touch_overlay =
(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);
}
// NOLEGACY causes WM_CHAR to be not received
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
+3 -1
View File
@@ -88,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
extern bool GRAPHICS_IIDX_WSUB;
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
@@ -157,7 +159,7 @@ bool graphics_gitadora_apply_window_monitor(
int &width,
int &height,
bool log_change);
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
bool graphics_gitadora_has_window_override(const std::string &window_name);
void change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot(
@@ -30,6 +30,8 @@ std::string GRAPHICS_GITADORA_MAIN_MONITOR;
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
bool GRAPHICS_IIDX_WSUB = false;
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
return empty;
}
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
bool graphics_gitadora_has_window_override(const std::string &window_name) {
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
return !monitor_name.empty();
return !monitor_name.empty() ||
(window_name == "SMALL" &&
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() ||
GRAPHICS_GITADORA_SMALL_POS.has_value()));
}
static bool graphics_monitor_rect_from_name(
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
int &width,
int &height,
bool log_change) {
return graphics_gitadora_apply_monitor_rect(
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false;
}
bool applied = graphics_gitadora_apply_monitor_rect(
graphics_gitadora_monitor_for_window_name(window_name),
window_name,
x,
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
width,
height,
log_change);
if (window_name != "SMALL") {
return applied;
}
std::pair<uint32_t, uint32_t> result;
if (GRAPHICS_GITADORA_SMALL_SIZE.has_value()) {
if (parse_width_height(GRAPHICS_GITADORA_SMALL_SIZE.value(), result)) {
width = result.first;
height = result.second;
applied = true;
} else {
log_fatal("graphics-windowed", "failed to parse -gdwsmallsize");
}
}
if (GRAPHICS_GITADORA_SMALL_POS.has_value()) {
if (parse_width_height(GRAPHICS_GITADORA_SMALL_POS.value(), result)) {
x = result.first;
y = result.second;
applied = true;
} else {
log_fatal("graphics-windowed", "failed to parse -gdwsmallpos");
}
}
if (applied && log_change &&
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() || GRAPHICS_GITADORA_SMALL_POS.has_value())) {
log_info(
"graphics-windowed",
"GITADORA SMALL custom override: pos=({}, {}), size={}x{}",
x,
y,
width,
height);
}
return applied;
}
void graphics_capture_initial_window(HWND hWnd) {
@@ -254,7 +299,7 @@ void graphics_capture_initial_window(HWND hWnd) {
cfg::SCREENRESIZE->init_client_width = client_w;
cfg::SCREENRESIZE->init_client_height = client_h;
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
log_debug(
log_misc(
"graphics-windowed",
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
fmt::ptr(hWnd),
+66 -1
View File
@@ -1,5 +1,6 @@
#include "external/nvapi/nvapi.h"
#include "external/nvapi/NvApiDriverSettings.h"
#include "avs/game.h"
#include "hooks/libraryhook.h"
#include "util/detour.h"
#include "util/libutils.h"
@@ -12,14 +13,21 @@ namespace nvapi_hook {
bool BYPASS_NVAPI = false;
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr;
static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr;
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
NvPhysicalGpuHandle hPhysicalGpu,
NV_GPU_DISPLAYIDS *pDisplayIds,
NvU32 *pDisplayIdCount,
NvU32 flags);
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
void initialize(HINSTANCE dll) {
#ifdef SPICE64
std::string nvapi_dll = "nvapi64.dll";
#else
@@ -40,6 +48,18 @@ namespace nvapi_hook {
return nullptr;
}
// spoof display connector types only for SDVX
if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) {
NvAPI_GPU_GetConnectedDisplayIds_orig =
reinterpret_cast<decltype(NvAPI_GPU_GetConnectedDisplayIds) *>(
NvAPI_QueryInterface_orig(func_code));
NvAPI_DISP_GetGDIPrimaryDisplayId_orig =
reinterpret_cast<decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *>(
NvAPI_QueryInterface_orig(0x1E9D8A31));
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked");
return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook;
}
// NvAPI_DISP_SetDisplayConfig
if (func_code == 0x5D8CF8DE) {
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
@@ -51,8 +71,53 @@ namespace nvapi_hook {
return NvAPI_QueryInterface_orig(func_code);
}
NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
NvPhysicalGpuHandle hPhysicalGpu,
NV_GPU_DISPLAYIDS *pDisplayIds,
NvU32 *pDisplayIdCount,
NvU32 flags) {
if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) {
return NVAPI_NO_IMPLEMENTATION;
}
const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig(
hPhysicalGpu,
pDisplayIds,
pDisplayIdCount,
flags);
if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr ||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) {
return status;
}
NvU32 primary_display_id = 0;
if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) {
return status;
}
// report the primary display adapter as DP and all other displays as HDMI
// (the game expects this since the VM cab is like that)
//
// this doesn't actually affect gameplay; only how
// NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz
// gets printed.
//
// for the actual value that matters for gameplay, it's this:
// NvDisplayConfig: GetMainDisplayRefreshRate = {}hz
for (NvU32 index = 0; index < *pDisplayIdCount; index++) {
auto &display = pDisplayIds[index];
display.connectorType = display.displayId == primary_display_id ?
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
}
return status;
}
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
// do not let the game apply its emulated cabinet topology to Windows
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
return NVAPI_OK;
}
+480
View File
@@ -0,0 +1,480 @@
#include "nvapi_impl.h"
#ifdef SPICE64
#include <algorithm>
#include <vector>
#include <windows.h>
#include "external/nvapi/nvapi.h"
#include "hooks/libraryhook.h"
#include "util/logging.h"
#include "util/sysutils.h"
namespace nvapi_impl {
namespace {
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
struct SyntheticDisplay {
NvU32 display_id;
NvU32 width;
NvU32 height;
NvU32 color_depth;
NvS32 x;
NvS32 y;
NvU32 refresh_rate_1k;
NV_ROTATE rotation;
bool primary;
};
static bool provider_initialized = false;
static bool nvapi_initialized = false;
static int gpu_handle_storage = 0;
// snapshot of the Win32 display state exposed through synthetic NVAPI
static std::vector<SyntheticDisplay> displays;
static NvPhysicalGpuHandle get_gpu_handle() {
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
}
static NV_ROTATE get_rotation(DWORD orientation) {
switch (orientation) {
case DMDO_90:
return NV_ROTATE_90;
case DMDO_180:
return NV_ROTATE_180;
case DMDO_270:
return NV_ROTATE_270;
default:
return NV_ROTATE_0;
}
}
static std::vector<SyntheticDisplay> enumerate_displays(
uint32_t main_refresh_hz,
uint32_t sub_refresh_hz) {
std::vector<SyntheticDisplay> result;
// reuse the active monitor list, then read live modes after -mainmonitor changes
for (const auto &monitor : sysutils::enumerate_monitors()) {
DEVMODEA mode {};
mode.dmSize = sizeof(mode);
if (!EnumDisplaySettingsExA(
monitor.display_name.c_str(),
ENUM_CURRENT_SETTINGS,
&mode,
0)) {
continue;
}
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
result.push_back({
.display_id = 0,
.width = mode.dmPelsWidth,
.height = mode.dmPelsHeight,
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
.x = mode.dmPosition.x,
.y = mode.dmPosition.y,
.refresh_rate_1k = 0,
.rotation = get_rotation(mode.dmDisplayOrientation),
.primary = primary,
});
}
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
return left.primary && !right.primary;
});
if (result.size() > 2) {
result.resize(2);
}
if (result.empty()) {
result.push_back({
.display_id = 0,
.width = 1920,
.height = 1080,
.color_depth = 32,
.x = 0,
.y = 0,
.refresh_rate_1k = 0,
.rotation = NV_ROTATE_0,
.primary = true,
});
}
for (size_t index = 0; index < result.size(); index++) {
auto &display = result[index];
display.primary = index == 0;
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
display.refresh_rate_1k = refresh_hz * 1000;
}
return result;
}
// initializes NVAPI for the calling process.
// marks the synthetic provider initialized without contacting a driver.
static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
log_misc("nvapi_impl", "NvAPI_Initialize");
nvapi_initialized = true;
return NVAPI_OK;
}
// initializes NVAPI with additional client flags.
// accepts the flags and marks the synthetic provider initialized.
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
nvapi_initialized = true;
return NVAPI_OK;
}
// releases NVAPI state held for the calling process.
// clears the synthetic initialization state while leaving the provider installed.
static NvAPI_Status __cdecl NvAPI_Unload_impl() {
log_misc("nvapi_impl", "NvAPI_Unload");
nvapi_initialized = false;
return NVAPI_OK;
}
// enumerates physical GPU handles managed by the NVIDIA driver.
// returns one stable synthetic GPU containing all exposed displays.
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
NvU32 *gpu_count) {
log_misc(
"nvapi_impl",
"NvAPI_EnumPhysicalGPUs(handles={}, count={})",
fmt::ptr(gpu_handles),
fmt::ptr(gpu_count));
if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED;
}
if (gpu_handles == nullptr || gpu_count == nullptr) {
return NVAPI_INVALID_ARGUMENT;
}
gpu_handles[0] = get_gpu_handle();
*gpu_count = 1;
log_misc(
"nvapi_impl",
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
fmt::ptr(gpu_handles[0]),
*gpu_count);
return NVAPI_OK;
}
// returns connected display descriptors for a physical GPU.
// exposes the monitor snapshot as DP primary and HDMI secondary displays.
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
NvPhysicalGpuHandle gpu_handle,
NV_GPU_DISPLAYIDS *display_ids,
NvU32 *display_id_count,
NvU32 flags) {
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
log_misc(
"nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
fmt::ptr(gpu_handle),
fmt::ptr(display_ids),
input_count,
flags);
if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED;
}
if (gpu_handle != get_gpu_handle()) {
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
}
if (display_id_count == nullptr) {
return NVAPI_INVALID_ARGUMENT;
}
const NvU32 required_count = static_cast<NvU32>(displays.size());
if (display_ids == nullptr) {
*display_id_count = required_count;
log_misc(
"nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
required_count);
return NVAPI_OK;
}
const NvU32 capacity = *display_id_count;
*display_id_count = required_count;
if (capacity < required_count) {
return NVAPI_INSUFFICIENT_BUFFER;
}
for (NvU32 index = 0; index < required_count; index++) {
const auto &source = displays[index];
auto &destination = display_ids[index];
destination = {};
destination.version = NV_GPU_DISPLAYIDS_VER;
destination.connectorType = source.primary ?
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
destination.displayId = source.display_id;
destination.isActive = 1;
destination.isOSVisible = 1;
destination.isConnected = 1;
destination.isPhysicallyConnected = 1;
}
log_misc(
"nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
required_count);
return NVAPI_OK;
}
// returns the NVAPI display ID associated with the Windows GDI primary.
// returns the first synthetic display, ordered from the live desktop origin.
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
log_misc(
"nvapi_impl",
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
fmt::ptr(display_id));
if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED;
}
if (display_id == nullptr || displays.empty()) {
return NVAPI_INVALID_ARGUMENT;
}
*display_id = displays.front().display_id;
log_misc(
"nvapi_impl",
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
*display_id);
return NVAPI_OK;
}
static void fill_source_mode(
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
const SyntheticDisplay &source) {
if (destination == nullptr) {
return;
}
*destination = {};
destination->resolution.width = source.width;
destination->resolution.height = source.height;
destination->resolution.colorDepth = source.color_depth;
destination->colorFormat = NV_FORMAT_A8R8G8B8;
destination->position.x = source.x;
destination->position.y = source.y;
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
destination->bGDIPrimary = source.primary ? 1 : 0;
}
static NvAPI_Status fill_target(
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
const SyntheticDisplay &source,
NvU32 target_id) {
if (destination == nullptr) {
return NVAPI_OK;
}
auto *details = destination->details;
destination->displayId = source.display_id;
destination->targetId = target_id;
if (details == nullptr) {
return NVAPI_OK;
}
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
}
*details = {};
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
details->rotation = source.rotation;
details->scaling = NV_SCALING_DEFAULT;
details->refreshRate1K = source.refresh_rate_1k;
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
return NVAPI_OK;
}
// retrieves the current global display topology through NVAPI's three-pass contract.
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
NvU32 *path_info_count,
NV_DISPLAYCONFIG_PATH_INFO *path_info) {
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
log_misc(
"nvapi_impl",
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
input_count,
fmt::ptr(path_info));
if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED;
}
if (path_info_count == nullptr) {
return NVAPI_INVALID_ARGUMENT;
}
const NvU32 required_count = static_cast<NvU32>(displays.size());
if (path_info == nullptr) {
*path_info_count = required_count;
log_misc(
"nvapi_impl",
"NvAPI_DISP_GetDisplayConfig - required_count={}",
required_count);
return NVAPI_OK;
}
const NvU32 capacity = *path_info_count;
*path_info_count = required_count;
if (capacity < required_count) {
return NVAPI_INSUFFICIENT_BUFFER;
}
for (NvU32 index = 0; index < required_count; index++) {
auto &path = path_info[index];
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
}
if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
return NVAPI_INSUFFICIENT_BUFFER;
}
const auto &display = displays[index];
path.sourceId = index;
path.targetInfoCount = 1;
path.IsNonNVIDIAAdapter = 0;
path.pOSAdapterID = nullptr;
fill_source_mode(path.sourceModeInfo, display);
const NvAPI_Status status = fill_target(path.targetInfo, display, index);
if (status != NVAPI_OK) {
return status;
}
}
log_misc(
"nvapi_impl",
"NvAPI_DISP_GetDisplayConfig - returned_count={}",
required_count);
return NVAPI_OK;
}
// applies a supplied global display topology through the NVIDIA driver.
// accepts the cabinet topology without making any changes to Windows.
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
NvU32 path_info_count,
NV_DISPLAYCONFIG_PATH_INFO *path_info,
NvU32 flags) {
log_misc(
"nvapi_impl",
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
path_info_count,
fmt::ptr(path_info),
flags);
if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED;
}
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
return NVAPI_OK;
}
template<typename T>
static uintptr_t *query_result(T function) {
return reinterpret_cast<uintptr_t *>(function);
}
// resolves an NVAPI function ID to its implementation address.
// exposes only the synthetic entry points used by KFC and rejects all others.
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
uintptr_t *result = nullptr;
switch (function_id) {
case NVAPI_INITIALIZE_ID:
result = query_result(NvAPI_Initialize_impl);
break;
case NVAPI_INITIALIZE_EX_ID:
result = query_result(NvAPI_InitializeEx_impl);
break;
case NVAPI_UNLOAD_ID:
result = query_result(NvAPI_Unload_impl);
break;
case NVAPI_ENUM_PHYSICAL_GPUS_ID:
result = query_result(NvAPI_EnumPhysicalGPUs_impl);
break;
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
break;
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
break;
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
break;
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
break;
default:
break;
}
log_misc(
"nvapi_impl",
"NvAPI_QueryInterface(0x{:x}) - {}",
function_id,
result != nullptr ? "implemented" : "unsupported");
return result;
}
}
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
if (provider_initialized) {
return true;
}
if (dll == nullptr) {
log_warning("nvapi_impl", "invalid synthetic module handle");
return false;
}
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
libraryhook_enable();
provider_initialized = true;
log_info(
"nvapi_impl",
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
NVAPI_DLL_NAME_A,
displays.size(),
main_refresh_hz,
sub_refresh_hz);
return true;
}
}
#endif
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#ifdef SPICE64
#include <cstdint>
#include <windows.h>
namespace nvapi_impl {
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
}
#endif
+16
View File
@@ -12,6 +12,7 @@ static robin_hood::unordered_map<std::string, FARPROC> PROCS;
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
return LoadLibraryW_orig(lpFileName);
}
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
// check hooks
if (lpFileName) {
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
}
}
// fallback
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
}
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
// check hooks
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
// detour
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
+26 -24
View File
@@ -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"
@@ -102,7 +102,7 @@
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "overlay/windows/patch_manager.h"
#include "patcher/patch_manager.h"
#include "overlay/windows/iidx_seg.h"
#include "overlay/windows/obs.h"
#include "rawinput/rawinput.h"
@@ -379,6 +379,7 @@ int main_implementation(int argc, char *argv[]) {
}
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
}
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
@@ -428,9 +429,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
D3D9_DEVICE_HOOK_DISABLE = true;
// touch emulation gets disabled, might as well turn these on
games::iidx::NATIVE_TOUCH = true;
games::sdvx::NATIVETOUCH = true;
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
overlay::ENABLED = false;
// leaving these on without dx9hooks result in torn state and therefore failure to draw
@@ -490,9 +488,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
attach_sdvx = true;
}
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
games::sdvx::NATIVETOUCH = true;
}
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
@@ -593,9 +588,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
games::iidx::DISABLE_ESPEC_IO = true;
}
if (options[launcher::Options::spice2x_IIDXNativeTouch].value_bool()) {
games::iidx::NATIVE_TOUCH = true;
}
// should come later since this will override a few settings
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
@@ -643,9 +635,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
}
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
games::popn::NATIVE_TOUCH = true;
}
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
SUBSCREEN_FORCE_REDRAW = true;
}
@@ -889,7 +878,7 @@ int main_implementation(int argc, char *argv[]) {
options[launcher::Options::ScreenResizeConfigPath].value_text();
}
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
options[launcher::Options::PatchManagerConfigPath].value_text();
}
if (options[launcher::Options::PathToAppConfig].is_active()) {
@@ -1286,6 +1275,14 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_GITADORA_SMALL_MONITOR =
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
}
if (options[launcher::Options::GitaDoraWindowedSmallSize].is_active()) {
GRAPHICS_GITADORA_SMALL_SIZE =
options[launcher::Options::GitaDoraWindowedSmallSize].value_text();
}
if (options[launcher::Options::GitaDoraWindowedSmallPosition].is_active()) {
GRAPHICS_GITADORA_SMALL_POS =
options[launcher::Options::GitaDoraWindowedSmallPosition].value_text();
}
// IIDX/SDVX Windowed Subscreen
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
@@ -1786,10 +1783,19 @@ 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;
games::nost::ENABLE_POKE = true;
}
if (options[launcher::Options::NostalgiaTouchMode].is_active()) {
if (overlay::ENABLED) {
games::nost::ENABLE_TOUCH_MODE = true;
} else {
log_warning(
"launcher",
"Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch");
}
}
}
@@ -2517,10 +2523,8 @@ int main_implementation(int argc, char *argv[]) {
// copy defaults to nvram
avs::core::copy_defaults();
// prepare patches
{
overlay::windows::PatchManager patch_manager(nullptr, false);
}
// prepare patches (registers the DLL-load notification before the game DLL loads)
patcher::init();
// load game
avs::game::load_dll();
@@ -2648,9 +2652,7 @@ int main_implementation(int argc, char *argv[]) {
}
// apply patches
{
overlay::windows::PatchManager patch_manager(nullptr, true);
}
patcher::apply_patches_on_start();
// load AVS-EA3
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
+49 -17
View File
@@ -962,13 +962,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Cab Peripherals",
},
{
.title = "SDVX FS Subscreen Native Touch Handling",
.title = "SDVX Native Touch (DEPRECATED - no longer needed)",
.name = "sdvxnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
"Requires a touch screen to be connected as a secondary monitor. "
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
"Enable this when you get duplicate touch inputs from an actual touch screen.",
.desc = "This option does nothing.\n\n"
"Native touch handling is now enabled by default and this option is no longer needed.",
.type = OptionType::Bool,
.hidden = true,
.game_name = "Sound Voltex",
.category = "Advanced Game Options",
},
@@ -1159,13 +1158,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// PopnNativeTouch
.title = "Pop'n Music PikaPika Native Touch Handling",
.title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
.name = "popnnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
"Requires a touch screen to be connected as a secondary monitor. "
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
"Enable this when you get duplicate touch inputs from an actual touch screen.",
.desc = "This option does nothing.\n\n"
"Native touch handling is now enabled by default and this option is no longer needed.",
.type = OptionType::Bool,
.hidden = true,
.game_name = "Pop'n Music",
.category = "Advanced Game Options",
},
@@ -1793,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",
},
@@ -2644,6 +2642,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Windowed Settings",
.picker = OptionPickerType::Monitor,
},
{
// GitaDoraWindowedSmallSize
.title = "GitaDora Windowed SMALL Size",
.name = "gdwsmallsize",
.desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).",
.type = OptionType::Text,
.setting_name = "800,1280",
.game_name = "GitaDora",
.category = "Game Windowed Settings",
},
{
// GitaDoraWindowedSmallPosition
.title = "GitaDora Windowed SMALL Position",
.name = "gdwsmallpos",
.desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).",
.type = OptionType::Text,
.setting_name = "0,0",
.game_name = "GitaDora",
.category = "Game Windowed Settings",
},
{
// spice2x_IIDXWindowedSubscreenSize
.title = "IIDX Windowed Subscreen Size",
@@ -2870,15 +2888,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// spice2x_IIDXNativeTouch
.title = "IIDX TDJ Subscreen Native Touch Handling",
.title = "IIDX Native Touch (DEPRECATED - no longer needed)",
.name = "sp2x-iidxnativetouch",
.display_name = "iidxnativetouch",
.aliases= "iidxnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
"Requires a touch screen to be connected as a secondary monitor. "
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
"Enable this when you get duplicate touch inputs from an actual touch screen.",
.desc = "This option does nothing.\n\n"
"Native touch handling is now enabled by default and this option is no longer needed.",
.type = OptionType::Bool,
.hidden = true,
.game_name = "Beatmania IIDX",
.category = "Advanced Game Options",
},
@@ -3132,6 +3149,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.game_name = "Nostalgia",
.category = "Game Options",
.quick_setting_category = "Game",
},
{
// NostalgiaTouchMode
.title = "Nostalgia Touch Piano",
.name = "nosttouch",
.desc =
"Allows you to play the piano by touching the screen instead of a controller. "
"Use the mode switch button to toggle between interacting with the menu and playing the piano.\n\n"
"Velocity sensitivity is not supported. Touch targets will be pixel-perfect aligned with the "
"judgement line, key beams, and notes.",
.type = OptionType::Bool,
.game_name = "Nostalgia",
.category = "Game Options",
.quick_setting_category = "Game",
},
{
// ForceBackBufferCount
+3
View File
@@ -258,6 +258,8 @@ namespace launcher {
GitaDoraWindowedLeftMonitor,
GitaDoraWindowedRightMonitor,
GitaDoraWindowedSmallMonitor,
GitaDoraWindowedSmallSize,
GitaDoraWindowedSmallPosition,
spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition,
IIDXWindowedSubscreenBorderless,
@@ -295,6 +297,7 @@ namespace launcher {
DDRP4IOBufferMode,
InputRequiresFocus,
NostalgiaPoke,
NostalgiaTouchMode,
ForceBackBufferCount,
SDVXWindowedSubscreenSize,
SDVXWindowedSubscreenPosition,
+401 -45
View File
@@ -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, &current)) {
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;
-122
View File
@@ -1,122 +0,0 @@
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "avs/game.h"
#include "wintouchemu.h"
#include "rawinput/touch.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/logging.h"
#define TOUCH_SIMULATE_FAT_FINGERS 0
#define TOUCH_DEBUG_VERBOSE 0
#if TOUCH_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
namespace nativetouchhook {
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
static void strip_contact_size(PTOUCHINPUT point) {
#if TOUCH_SIMULATE_FAT_FINGERS
point->dwMask |= 0x004;
point->cxContact = 80 * 100;
point->cyContact = 60 * 100;
#endif
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
// monitors that do set it, IIDX can get very confused (SDVX is not
// affected)
//
// while the test menu and the touch "glow" seem to work properly,
// interacting with subscreen menu items or entering PIN becomes
// very unpredictable
//
// to fix this, simply remove the contact area width and height
//
// note: test menu > I/O > touch test gives 5 numbers:
// n: x, y, w, h
// where
// n is the nth touch input since boot
// x, y are coordinates (center of finger)
// w, h are contact width and height
//
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
// automatically be seen as 1x1, which works perfectly fine
log_debug(
"touch::native",
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
point->x / 100,
point->y / 100,
point->dwMask,
point->cxContact,
point->cyContact);
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
point->cxContact = 0;
point->cyContact = 0;
}
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(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// call the original fist
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
if (result == 0) {
return result;
}
bool flip_values = false;
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
log_debug(
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
rawinput::touch::DISPLAY_ORIENTATION,
rawinput::touch::DISPLAY_SIZE_X,
rawinput::touch::DISPLAY_SIZE_Y);
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_values = true;
}
}
if (rawinput::touch::INVERTED) {
flip_values = !flip_values;
}
for (size_t i = 0; i < cInputs; i++) {
PTOUCHINPUT point = &pInputs[i];
if (avs::game::is_model("LDJ")) {
strip_contact_size(point);
}
if (flip_values) {
flip_touch_points(point);
}
}
return result;
}
void hook(HMODULE module) {
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
if (GetTouchInputInfo_orig != nullptr) {
log_misc("touch::native", "GetTouchInputInfo hooked");
}
}
}
-6
View File
@@ -1,6 +0,0 @@
#include <windows.h>
namespace nativetouchhook {
void hook(HMODULE module);
}
+24 -1
View File
@@ -5,6 +5,7 @@
#include "games/io.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/nost/nost.h"
#include "games/popn/popn.h"
#include "games/rb/touch_debug.h"
#include "hooks/graphics/graphics.h"
@@ -51,6 +52,7 @@
#include "windows/sdvx_sub.h"
#include "windows/keypad.h"
#include "windows/log.h"
#include "windows/nostalgia_touch_piano.h"
#include "windows/obs.h"
#include "windows/patch_manager.h"
#include "windows/exitprompt.cpp"
@@ -404,6 +406,12 @@ void overlay::SpiceOverlay::init() {
window_fps->set_active(true);
}
if (!cfg::CONFIGURATOR_STANDALONE &&
avs::game::is_model("PAN") && games::nost::ENABLE_TOUCH_MODE) {
window_nostalgia_touch_piano =
std::make_unique<overlay::windows::NostalgiaTouchPiano>(this);
}
// owned separately from `windows` so it is not part of the overlay window layer
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
@@ -556,13 +564,18 @@ void overlay::SpiceOverlay::new_frame() {
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
&& this->window_fps->get_active();
const bool draw_nostalgia_touch_piano = this->renderer != OverlayRenderer::SOFTWARE
&& this->window_nostalgia_touch_piano
&& this->window_nostalgia_touch_piano->get_active();
// draw RB touch diagnostics
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
&& games::rb::touch_debug_overlay_enabled();
// check if there is nothing to draw
this->has_pending_frame = false;
if (!this->active && !draw_notifications && !draw_fps_persistent && !draw_rb_touch_debug) {
if (!this->active && !draw_notifications && !draw_fps_persistent &&
!draw_nostalgia_touch_piano && !draw_rb_touch_debug) {
return;
}
@@ -596,7 +609,13 @@ void overlay::SpiceOverlay::new_frame() {
for (auto &window : this->windows) {
window->build();
}
}
if (draw_nostalgia_touch_piano) {
this->window_nostalgia_touch_piano->build();
}
if (this->active) {
// draw the main menu on top of the overlay windows
this->window_main_menu->build();
@@ -819,6 +838,10 @@ void overlay::SpiceOverlay::update() {
// FPS window
this->window_fps->update();
if (this->window_nostalgia_touch_piano) {
this->window_nostalgia_touch_piano->update();
}
// main menu (owned separately from the overlay window layer)
this->window_main_menu->update();
+4
View File
@@ -83,6 +83,7 @@ namespace overlay {
// not part of `windows`: drawn/updated on the persistent layer (like
// notifications), independent of the overlay's active state and input gates.
std::unique_ptr<Window> window_fps;
std::unique_ptr<Window> window_nostalgia_touch_piano;
// not part of `windows`: the main menu / launcher. owned and drawn
// separately from the overlay window layer; it drives the overlay's
@@ -138,6 +139,9 @@ namespace overlay {
inline IDirect3DDevice9 *get_device() {
return this->device;
}
inline HWND get_window() {
return this->hWnd;
}
bool can_transform_touch_input() {
return (this->subscreen_mouse_handler != nullptr);
+190 -48
View File
@@ -85,6 +85,7 @@ namespace overlay::windows {
static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = {
{ "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS },
{ "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS },
{ "Modifiers", ControllerPage::CONTROLLER_PAGE_MODIFIERS },
{ "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS },
{ "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY },
{ "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS },
@@ -251,7 +252,7 @@ namespace overlay::windows {
}
// Renders an arrow-less, non-collapsible left-nav header row, highlighted when
// active. Returns true if the row was clicked this frame; callers apply their
// active. Returns true if the row was activated this frame; callers apply their
// own selection side effects.
bool Config::build_nav_header(const char *label, bool active) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf;
@@ -282,7 +283,7 @@ namespace overlay::windows {
ImGui::PopStyleColor(colors_pushed);
}
return ImGui::IsItemClicked();
return ImGui::IsItemActivated();
}
void Config::build_options_tab(float page_offset) {
@@ -616,6 +617,21 @@ namespace overlay::windows {
}
break;
}
case ControllerPage::CONTROLLER_PAGE_MODIFIERS: {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Modifiers");
ImGui::Spacing();
ImGui::TextWrapped(
"Bind modifier buttons below, then open Edit properties on any button binding "
"(except MIDI) to select the required modifiers. This allows you to configure "
"button combinations.\n\n"
"For example, if you bind the Start button as Modifier 1, and then bind "
"the A button for Service Coin and set it to require Modifier 1, you press "
"Start + A to trigger Service Coin.");
ImGui::TextUnformatted("");
this->build_buttons(
"Modifier", games::get_buttons_modifiers(this->games_selected_name));
break;
}
case ControllerPage::CONTROLLER_PAGE_ANALOGS: {
// help text for binding analog, if the game has one
@@ -1256,7 +1272,8 @@ namespace overlay::windows {
if (open_edit) {
ImGui::OpenPopup(edit_name.c_str());
}
edit_button_popup(edit_name, button_display, button, button_velocity, alt_index);
edit_button_popup(
edit_name, button_display, button, button_velocity, alt_index, name != "Modifier");
// clean up
ImGui::PopID();
@@ -1275,6 +1292,7 @@ namespace overlay::windows {
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setInvert(false);
button->setModifierMask(0);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
button->setLastVelocity(0);
button->setTemporary(false);
@@ -1356,8 +1374,9 @@ namespace overlay::windows {
if (check_devices) {
// iterate updated devices
auto updated_devices = RI_MGR->devices_get_updated();
bool binding_finished = false;
for (auto device : updated_devices) {
std::lock_guard<std::mutex> lock(*device->mutex);
std::unique_lock<std::mutex> lock(*device->mutex);
switch (device->type) {
case rawinput::MOUSE: {
auto mouse = device->mouseInfo;
@@ -1369,12 +1388,13 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
}
@@ -1393,7 +1413,7 @@ namespace overlay::windows {
buttons_bind_active = false;
buttons_many_index = -1;
buttons_many_active = false;
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
@@ -1401,12 +1421,13 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name);
button->setVKey(vkey);
button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
}
@@ -1536,13 +1557,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
}
break;
@@ -1581,6 +1603,7 @@ namespace overlay::windows {
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setModifierMask(0);
// same idea as setMidiVKey - keep velocity threshold consistent
button->setVelocityThreshold(
device->midiInfo->v2_velocity_threshold[button->getVKey()]);
@@ -1590,7 +1613,7 @@ namespace overlay::windows {
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
}
@@ -1608,13 +1631,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
} else {
@@ -1635,13 +1659,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
} else {
@@ -1662,13 +1687,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
} else {
@@ -1688,13 +1714,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
@@ -1709,13 +1736,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
}
@@ -1739,13 +1767,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
binding_finished = true;
break;
}
}
@@ -1755,6 +1784,13 @@ namespace overlay::windows {
default:
break;
}
lock.unlock();
if (binding_finished) {
break;
}
}
if (binding_finished) {
RI_MGR->devices_midi_freeze(false);
}
}
@@ -1808,6 +1844,7 @@ namespace overlay::windows {
if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) {
button->setDeviceIdentifier(xinput::get_device_desc(xinput.player));
button->setVKey(static_cast<uint16_t>(xinput.button));
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1);
inc_buttons_many_index(button_it_max);
@@ -1864,6 +1901,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
@@ -1882,27 +1920,103 @@ namespace overlay::windows {
}
}
bool Config::build_modifier_picker(Button &button) {
bool changed = false;
auto modifier_mask = button.getModifierMask();
// collect live states and build the closed dropdown summary
uint8_t pressed_modifier_mask = 0;
std::string modifier_preview;
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
const uint8_t modifier_bit = UINT8_C(1) << index;
if (modifier_buttons && index < modifier_buttons->size() &&
GameAPI::Buttons::getState(RI_MGR, modifier_buttons->at(index))) {
pressed_modifier_mask |= modifier_bit;
}
if ((modifier_mask & modifier_bit) != 0) {
modifier_preview += modifier_preview.empty() ? "Mod " : "+";
modifier_preview += std::to_string(index + 1);
}
}
if (modifier_preview.empty()) {
modifier_preview = "None";
}
// color the combo when all requirements are held, but keep its label unchanged
const bool all_modifiers_pressed = modifier_mask != 0 &&
(pressed_modifier_mask & modifier_mask) == modifier_mask;
if (all_modifiers_pressed) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
const bool combo_open = ImGui::BeginCombo("##Modifiers", modifier_preview.c_str());
if (all_modifiers_pressed) {
ImGui::PopStyleColor();
}
// update requirements while highlighting modifier sources that are currently pressed
if (combo_open) {
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
const uint8_t modifier_bit = UINT8_C(1) << index;
bool required = (modifier_mask & modifier_bit) != 0;
const auto label = fmt::format("Modifier {}", index + 1);
const bool modifier_pressed = (pressed_modifier_mask & modifier_bit) != 0;
if (modifier_pressed) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
const bool requirement_changed = ImGui::Checkbox(label.c_str(), &required);
if (modifier_pressed) {
ImGui::PopStyleColor();
}
if (requirement_changed) {
modifier_mask ^= modifier_bit;
button.setModifierMask(modifier_mask);
changed = true;
}
}
ImGui::EndCombo();
}
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::TextUnformatted("Modifiers");
return changed;
}
void Config::edit_button_popup(
const std::string &edit_name,
const std::string &button_display,
Button *button,
const float button_velocity,
const int alt_index) {
const int alt_index,
const bool allow_modifiers) {
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
bool dirty = false;
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
auto switch_device = [&](const std::string &device_identifier) {
button->setDeviceIdentifier(device_identifier);
button->setAnalogType(ButtonAnalogType::BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setVKey(0);
button->setInvert(false);
button->setModifierMask(0);
dirty = true;
};
// binding
ImGui::Text("Binding");
ImGui::Separator();
// combo for devices
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) {
button->setDeviceIdentifier("");
dirty = true;
if (ImGui::Selectable("Empty (Naive)", button->isNaive()) && !button->isNaive()) {
switch_device("");
}
if (button->isNaive()) {
ImGui::SetItemDefaultFocus();
@@ -1910,17 +2024,8 @@ namespace overlay::windows {
for (auto &device : RI_MGR->devices_get()) {
bool selected = button->getDeviceIdentifier() == device.name.c_str();
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
if (ImGui::Selectable(device_desc.c_str(), selected)) {
button->setDeviceIdentifier(device.name);
// reset controls when switching devices
button->setAnalogType(ButtonAnalogType::BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
button->setVKey(0);
button->setInvert(false);
dirty = true;
if (ImGui::Selectable(device_desc.c_str(), selected) && !selected) {
switch_device(device.name);
}
if (selected) {
ImGui::SetItemDefaultFocus();
@@ -1928,6 +2033,7 @@ namespace overlay::windows {
}
ImGui::EndCombo();
}
device = RI_MGR->devices_get(button->getDeviceIdentifier());
// analog type (only for HID)
const auto bat = button->getAnalogType();
@@ -2219,21 +2325,22 @@ namespace overlay::windows {
}
}
// modifiers
{
const bool is_midi = device != nullptr && device->type == rawinput::MIDI;
if (allow_modifiers && button->isValid() && !is_midi) {
dirty |= build_modifier_picker(*button);
}
}
// preview
if (!button_display.empty()) {
ImGui::TextUnformatted("\nPreview");
ImGui::Separator();
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
}
// this doesn't account for utf-8 but whatever
if (button_display.size() >= 40) {
ImGui::Text("%.37s...", button_display.c_str());
ImGui::SameLine();
ImGui::HelpMarker(button_display.c_str());
} else {
ImGui::TextUnformatted(button_display.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
ImGui::TextTruncated(button_display, ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted("\n");
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PopStyleColor();
@@ -2242,8 +2349,10 @@ namespace overlay::windows {
ImGui::TextUnformatted("");
}
// options
ImGui::Text("Options");
ImGui::TextUnformatted("Options");
ImGui::Separator();
// check for debounce
if (button->getDebounceUp() || button->getDebounceDown()
@@ -2362,6 +2471,7 @@ namespace overlay::windows {
ImGui::ProgressBar(button_velocity);
} else {
ImGui::Text("State");
ImGui::Separator();
ImGui::ProgressBar(button_velocity);
}
@@ -5482,6 +5592,12 @@ namespace overlay::windows {
tmpl.buttons.emplace_back(btn);
}
}
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
if (modifier_buttons) {
for (auto &modifier : *modifier_buttons) {
tmpl.modifier_buttons.emplace_back(modifier);
}
}
// capture keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
@@ -5537,6 +5653,17 @@ namespace overlay::windows {
}
}
// clear all modifier buttons
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
if (modifier_buttons) {
for (auto &modifier : *modifier_buttons) {
for (int ai = (int)modifier.getAlternatives().size() - 1; ai >= 0; ai--) {
clear_button(&modifier.getAlternatives()[ai], ai + 1);
}
clear_button(&modifier, -1);
}
}
// clear all keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
if (keypad_buttons) {
@@ -5630,6 +5757,7 @@ namespace overlay::windows {
btn.setDebounceDown(entry->debounce_down);
btn.setBatThreshold(entry->bat_threshold);
btn.setVelocityThreshold(entry->velocity_threshold);
btn.setModifierMask(entry->modifier_mask);
::Config::getInstance().updateBinding(game, btn, -1);
} else {
Button alt_btn(btn.getName());
@@ -5641,6 +5769,7 @@ namespace overlay::windows {
alt_btn.setDebounceDown(entry->debounce_down);
alt_btn.setBatThreshold(entry->bat_threshold);
alt_btn.setVelocityThreshold(entry->velocity_threshold);
alt_btn.setModifierMask(entry->modifier_mask);
alt_btn.setTemporary(true);
btn.getAlternatives().push_back(alt_btn);
::Config::getInstance().updateBinding(
@@ -5659,6 +5788,11 @@ namespace overlay::windows {
if (this->apply_buttons) {
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name));
}
if (this->apply_modifiers) {
apply_buttons(
tmpl.modifier_buttons,
games::get_buttons_modifiers(this->games_selected_name));
}
if (this->apply_keypads) {
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
}
@@ -5900,7 +6034,7 @@ namespace overlay::windows {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::HelpMarker(
"Clears all game button, analog, and light bindings.");
"Clears all game button, modifier, keypad, analog, and light bindings.");
if (this->all_cleared) {
ImGui::SameLine();
ImGui::TextUnformatted("Done.");
@@ -5915,6 +6049,8 @@ namespace overlay::windows {
bool selection_changed = false;
selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Modifiers", &this->apply_modifiers);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs);
@@ -5924,7 +6060,7 @@ namespace overlay::windows {
std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false);
}
if (!this->apply_buttons && !this->apply_keypads &&
if (!this->apply_buttons && !this->apply_modifiers && !this->apply_keypads &&
!this->apply_analogs && !this->apply_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n");
}
@@ -5964,7 +6100,8 @@ namespace overlay::windows {
}
}
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
!apply_analogs && !apply_lights) {
ImGui::BeginDisabled();
}
@@ -6133,7 +6270,8 @@ namespace overlay::windows {
ImGui::EndTable();
}
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
!apply_analogs && !apply_lights) {
ImGui::EndDisabled();
}
}
@@ -6212,12 +6350,14 @@ namespace overlay::windows {
ImGui::TextUnformatted("Pick which groups to save:");
ImGui::Checkbox("Buttons", &this->save_buttons);
ImGui::SameLine();
ImGui::Checkbox("Modifiers", &this->save_modifiers);
ImGui::SameLine();
ImGui::Checkbox("Keypads", &this->save_keypads);
ImGui::SameLine();
ImGui::Checkbox("Analogs", &this->save_analogs);
ImGui::SameLine();
ImGui::Checkbox("Lights", &this->save_lights);
if (!this->save_analogs && !this->save_keypads &&
if (!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
!this->save_analogs && !this->save_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n");
}
@@ -6269,7 +6409,8 @@ namespace overlay::windows {
ImGui::BeginDisabled(
!all_labels_set ||
(!this->save_buttons && !this->save_keypads && !this->save_analogs && !this->save_lights));
(!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
!this->save_analogs && !this->save_lights));
if (ImGui::Button("Save")) {
// replace device IDs with labels in the template
@@ -6278,7 +6419,8 @@ namespace overlay::windows {
template_save_sources[si], template_save_labels[si]);
}
if (save_user_template(template_pending_save,
this->save_buttons, this->save_keypads, this->save_analogs, this->save_lights)) {
this->save_buttons, this->save_modifiers, this->save_keypads,
this->save_analogs, this->save_lights)) {
template_save_name[0] = '\0';
templates_cache_dirty = true;
}
@@ -6360,7 +6502,7 @@ namespace overlay::windows {
t.rename_source(template_save_sources[si], template_save_labels[si]);
}
}
save_user_template(t, true, true, true, true);
save_user_template(t, true, true, true, true, true);
templates_cache_dirty = true;
template_save_sources.clear();
template_save_labels.clear();
+6 -1
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
CONTROLLER_PAGE_INVALID,
CONTROLLER_PAGE_BUTTONS,
CONTROLLER_PAGE_KEYPADS,
CONTROLLER_PAGE_MODIFIERS,
CONTROLLER_PAGE_ANALOGS,
CONTROLLER_PAGE_OVERLAY,
CONTROLLER_PAGE_LIGHTS,
@@ -119,10 +120,12 @@ namespace overlay::windows {
std::vector<std::string> template_save_sources;
std::vector<std::string> template_save_labels;
bool apply_buttons = true;
bool apply_modifiers = true;
bool apply_keypads = true;
bool apply_analogs = true;
bool apply_lights = true;
bool save_buttons = true;
bool save_modifiers = true;
bool save_keypads = true;
bool save_analogs = true;
bool save_lights = true;
@@ -168,12 +171,14 @@ namespace overlay::windows {
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max, const int alt_index);
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max, const int alt_index);
bool build_modifier_picker(Button &button);
void edit_button_popup(
const std::string &edit_name,
const std::string &button_display,
Button *button,
const float button_velocity,
const int alt_index);
const int alt_index,
const bool allow_modifiers);
void clear_button(Button *button, const int alt_index, std::optional<unsigned short> vKey_default = std::nullopt);
void reset_button_to_default(Button *button, unsigned short vKey_default);
unsigned int get_keypad_top_row(const Button &button);
@@ -6,7 +6,7 @@
namespace overlay::windows {
// helpers - iterate both buttons and keypad_buttons
// helpers - iterate all button groups
static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
for (auto &btn : btns) {
if (btn.primary.is_naive()) naive++;
@@ -21,6 +21,7 @@ namespace overlay::windows {
int ControllerTemplate::count_naive_buttons() const {
int naive = 0, device = 0;
count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device);
return naive;
}
@@ -28,6 +29,7 @@ namespace overlay::windows {
int ControllerTemplate::count_device_buttons() const {
int naive = 0, device = 0;
count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device);
return device;
}
@@ -64,6 +66,7 @@ namespace overlay::windows {
std::set<std::string> ControllerTemplate::get_used_devices() const {
std::set<std::string> devices;
collect_devices_from(buttons, devices);
collect_devices_from(modifier_buttons, devices);
collect_devices_from(keypad_buttons, devices);
for (auto &a : analogs) {
if (a.is_device()) devices.insert(a.device_identifier);
@@ -94,6 +97,7 @@ namespace overlay::windows {
bool has_naive = false;
collect_sources_from(buttons, sources_set, has_naive);
collect_sources_from(modifier_buttons, sources_set, has_naive);
collect_sources_from(keypad_buttons, sources_set, has_naive);
for (auto &a : analogs) {
if (a.is_device()) {
@@ -131,6 +135,7 @@ namespace overlay::windows {
}
};
rename_in_buttons(buttons);
rename_in_buttons(modifier_buttons);
rename_in_buttons(keypad_buttons);
for (auto &a : analogs) {
if (a.device_identifier == old_id) a.device_identifier = new_id;
@@ -181,6 +186,16 @@ namespace overlay::windows {
}
}
// modifier buttons
std::vector<std::string> modifier_lines;
collect_btn_lines(modifier_buttons, modifier_lines);
if (!modifier_lines.empty()) {
result += "Modifiers:\n";
for (auto &line : modifier_lines) {
result += " " + line + "\n";
}
}
// keypad buttons
std::vector<std::string> kp_lines;
collect_btn_lines(keypad_buttons, kp_lines);
@@ -19,6 +19,7 @@ namespace overlay::windows {
double debounce_down = 0.0;
int bat_threshold = 0;
unsigned short velocity_threshold = 0;
uint8_t modifier_mask = 0;
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
bool is_device() const { return !device_identifier.empty(); }
@@ -34,6 +35,7 @@ namespace overlay::windows {
e.debounce_down = btn.getDebounceDown();
e.bat_threshold = btn.getBatThreshold();
e.velocity_threshold = btn.getVelocityThreshold();
e.modifier_mask = btn.getModifierMask();
return e;
}
};
@@ -123,6 +125,7 @@ namespace overlay::windows {
std::string game_name;
bool is_builtin = false;
std::vector<TemplateButtonBinding> buttons;
std::vector<TemplateButtonBinding> modifier_buttons;
std::vector<TemplateButtonBinding> keypad_buttons;
std::vector<TemplateAnalogBinding> analogs;
std::vector<TemplateLightBinding> lights;
@@ -150,6 +153,7 @@ namespace overlay::windows {
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights);
@@ -0,0 +1,149 @@
#include "nostalgia_touch_piano.h"
#include <cmath>
#include "external/imgui/imgui_internal.h"
#include "games/nost/touch_mode.h"
namespace overlay::windows {
static constexpr float BUTTON_WIDTH = 144.f;
static constexpr float BUTTON_HEIGHT = 40.f;
static constexpr float WINDOW_PADDING = 4.f;
static constexpr float EDGE_MARGIN = 4.f;
static constexpr float PIANO_HEIGHT_RATIO = 0.08f;
static constexpr float PIANO_LEFT_GAP = 11.f;
static constexpr float PIANO_RIGHT_GAP = 10.f;
static constexpr uint32_t PIANO_KEY_COUNT = 28;
static constexpr ImU32 PIANO_KEY_COLOR = IM_COL32(255, 255, 255, 50);
static constexpr ImU32 PIANO_KEY_ACTIVE_COLOR = IM_COL32(255, 48, 48, 160);
static constexpr ImU32 PIANO_KEY_BORDER_COLOR = IM_COL32(0, 0, 0, 100);
struct ButtonPalette {
ImVec4 normal;
ImVec4 hovered;
ImVec4 active;
};
static const ButtonPalette NAV_MODE_PALETTE {
ImVec4(0.10f, 0.45f, 0.28f, 0.72f),
ImVec4(0.14f, 0.58f, 0.36f, 0.82f),
ImVec4(0.08f, 0.34f, 0.21f, 0.90f),
};
static const ButtonPalette PIANO_MODE_PALETTE {
ImVec4(0.15f, 0.32f, 0.62f, 0.72f),
ImVec4(0.20f, 0.42f, 0.78f, 0.82f),
ImVec4(0.10f, 0.24f, 0.50f, 0.90f),
};
static void draw_piano_keys(
const ImVec2 &display_size,
LONG client_width,
uint32_t key_state) {
if (display_size.x <= 0.f || display_size.y <= 0.f || client_width <= 0) {
return;
}
// this is only a visual guide; native touch routing owns the actual input
const float left_gap = PIANO_LEFT_GAP * display_size.x / client_width;
const float right_gap = PIANO_RIGHT_GAP * display_size.x / client_width;
const float piano_width = display_size.x - left_gap - right_gap;
if (piano_width <= 0.f) {
return;
}
const float key_width = piano_width / PIANO_KEY_COUNT;
const float key_top = display_size.y * (1.f - PIANO_HEIGHT_RATIO);
auto *draw_list = ImGui::GetBackgroundDrawList();
for (uint32_t key = 0; key < PIANO_KEY_COUNT; key++) {
const ImVec2 key_min(left_gap + key * key_width, key_top);
const ImVec2 key_max(left_gap + (key + 1) * key_width, display_size.y);
const bool active = (key_state & (UINT32_C(1) << key)) != 0;
draw_list->AddRectFilled(
key_min,
key_max,
active ? PIANO_KEY_ACTIVE_COLOR : PIANO_KEY_COLOR);
draw_list->AddRect(key_min, key_max, PIANO_KEY_BORDER_COLOR);
}
}
NostalgiaTouchPiano::NostalgiaTouchPiano(SpiceOverlay *overlay) : Window(overlay) {
this->title = "Nostalgia Touch Piano";
this->flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoDocking
| ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoBringToFrontOnFocus;
this->window_padding = overlay::apply_scaling_to_vector(WINDOW_PADDING, WINDOW_PADDING);
this->set_active(true);
}
void NostalgiaTouchPiano::calculate_initial_window() {
this->init_size = overlay::apply_scaling_to_vector(
BUTTON_WIDTH + WINDOW_PADDING * 2,
BUTTON_HEIGHT + WINDOW_PADDING * 2);
this->init_pos = overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN);
}
void NostalgiaTouchPiano::build_content() {
// keep the control anchored while the game window changes size or mode
ImGui::SetWindowPos(
overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN),
ImGuiCond_Always);
// stay above regular overlay windows, but never cover a blocking modal
ImGuiWindow *mode_window = ImGui::GetCurrentWindow();
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
ImGui::BringWindowToDisplayBehind(mode_window, modal);
} else {
ImGui::BringWindowToDisplayFront(mode_window);
}
const bool nav_mode =
games::nost::touch_mode::current_mode() == games::nost::touch_mode::Mode::Nav;
const char *label = nav_mode ? "Nav Mode" : "Piano Mode";
// make the active routing mode recognizable without reading the label
const auto &palette = nav_mode ? NAV_MODE_PALETTE : PIANO_MODE_PALETTE;
ImGui::PushStyleColor(ImGuiCol_Button, palette.normal);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette.hovered);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette.active);
ImGui::Button(label, overlay::apply_scaling_to_vector(BUTTON_WIDTH, BUTTON_HEIGHT));
ImGui::PopStyleColor(3);
const auto &io = ImGui::GetIO();
RECT client_rect {};
if (io.DisplaySize.x > 0.f && io.DisplaySize.y > 0.f &&
GetClientRect(this->overlay->get_window(), &client_rect)) {
// convert the rendered imgui rectangle into the client coordinates
// used by hardware touch publication and piano-key mapping
const auto item_min = ImGui::GetItemRectMin();
const auto item_max = ImGui::GetItemRectMax();
const auto client_width = client_rect.right - client_rect.left;
const auto client_height = client_rect.bottom - client_rect.top;
if (!nav_mode) {
draw_piano_keys(
io.DisplaySize,
client_width,
games::nost::touch_mode::piano_key_state());
}
RECT button_bounds {
static_cast<LONG>(std::lround(item_min.x * client_width / io.DisplaySize.x)),
static_cast<LONG>(std::lround(item_min.y * client_height / io.DisplaySize.y)),
static_cast<LONG>(std::lround(item_max.x * client_width / io.DisplaySize.x)),
static_cast<LONG>(std::lround(item_max.y * client_height / io.DisplaySize.y)),
};
games::nost::touch_mode::publish_button_bounds(
this->overlay->get_window(), button_bounds);
}
}
}
@@ -0,0 +1,15 @@
#pragma once
#include "overlay/window.h"
namespace overlay::windows {
// persistent mode control rendered independently of the main overlay visibility
class NostalgiaTouchPiano : public Window {
public:
explicit NostalgiaTouchPiano(SpiceOverlay *overlay);
void calculate_initial_window() override;
void build_content() override;
};
}
File diff suppressed because it is too large Load Diff
+3 -165
View File
@@ -1,183 +1,21 @@
#pragma once
#include "overlay/window.h"
#include <map>
#include <functional>
#include <filesystem>
#include <optional>
#include "external/rapidjson/document.h"
#include "patcher/patch_manager.h"
namespace overlay::windows {
enum class PatchType {
Unknown,
Memory,
Signature,
Union,
Integer,
};
enum class PatchStatus {
Error,
Disabled,
Enabled,
};
enum class PatchUrlStatus {
Valid,
Invalid,
Unapplied,
ValidButNoData,
Partial,
};
struct MemoryPatch {
std::string dll_name = "";
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
size_t data_disabled_len = 0;
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
size_t data_enabled_len = 0;
uint64_t data_offset = 0;
uint8_t *data_offset_ptr = nullptr;
bool fatal_error = false;
};
struct PatchData;
struct SignaturePatch {
std::string dll_name = "";
std::string signature = "", replacement = "";
uint64_t offset = 0;
int64_t usage = 0;
MemoryPatch to_memory(PatchData *patch);
};
struct UnionPatch {
std::string name = "";
std::string dll_name = "";
std::shared_ptr<uint8_t[]> data = nullptr;
size_t data_len = 0;
uint64_t offset = 0;
uint8_t* data_offset_ptr = nullptr;
bool fatal_error = false;
};
struct NumberPatch {
std::string dll_name = "";
uint64_t data_offset = 0;
uint8_t* data_offset_ptr = nullptr;
int32_t min;
int32_t max;
int32_t value;
size_t size_in_bytes;
bool fatal_error = false;
};
struct PatchData {
bool enabled;
std::string game_code;
int datecode_min = 0;
int datecode_max = 0;
std::string name, description, caution;
std::string name_in_lower_case = "";
PatchType type;
bool preset;
std::vector<MemoryPatch> patches_memory;
std::vector<UnionPatch> patches_union;
NumberPatch patch_number;
PatchStatus last_status;
std::string hash;
bool unverified = false;
std::string peIdentifier;
std::string error_reason = "";
// for union patch only
std::string selected_union_name = "";
};
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info=false);
class PatchManager : public Window {
public:
PatchManager(SpiceOverlay *overlay, bool apply_patches = false);
explicit PatchManager(SpiceOverlay *overlay);
~PatchManager() override;
void build_content() override;
void reload_local_patches(bool apply_patches = false);
bool import_remote_patches_to_disk();
bool load_from_patches_json(bool apply_patches);
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
void hard_apply_patches();
void load_embedded_patches(bool apply_patches);
private:
// configuration
static std::filesystem::path config_path;
static bool config_dirty;
static bool setting_auto_apply;
static std::vector<std::string> setting_auto_apply_list;
static std::vector<std::string> setting_patches_enabled;
static std::map<std::string, std::string> setting_union_patches_enabled;
static std::map<std::string, int64_t> setting_int_patches_enabled;
static std::string patch_url;
static std::string patch_name_filter;
static std::filesystem::path LOCAL_PATCHES_PATH;
static std::string ACTIVE_JSON_FILE;
// patches
static std::vector<PatchData> patches;
static bool local_patches_initialized;
// cached sorted view of `patches` for the table, stored as indices so a
// stale entry can never dangle if `patches` is rebuilt; rebuilt only when
// the sort order changes or the patch list is reloaded (not every frame)
static std::vector<size_t> patches_sorted;
void config_load();
void config_save();
// rebuild the cached sorted view of `patches` from the active table's
// sort specs; no-op unless the sort changed or the patch list changed
void update_sorted_patches();
void append_patches(
std::string &patches_json,
bool apply_patches = false,
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
std::string pe_identifier_for_patch = "");
void show_patch_tooltip(const PatchData& patch);
bool is_game_id_wildcard_matched(const std::string& id_from_config);
void show_patch_tooltip(const patcher::PatchData& patch);
};
PatchStatus is_patch_active(PatchData &patch);
bool apply_patch(PatchData &patch, bool active);
int64_t parse_little_endian_int(uint8_t* bytes, size_t size);
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size);
std::vector<uint8_t>* find_in_dll_map(
const std::string& dll_name, size_t offset, size_t size);
std::vector<uint8_t>* find_in_dll_map_org(
const std::string& dll_name, size_t offset, size_t size);
bool restore_bytes_from_dll_map_org(
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size);
void create_dll_backup(
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
std::string fix_up_dll_name(const std::string& dll_name);
uint8_t* get_dll_offset_for_patch_apply(
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes);
uint64_t parse_json_data_offset(
const std::string &patch_name, const rapidjson::Value &value);
void print_auto_apply_status(PatchData &patch);
}
+265
View File
@@ -0,0 +1,265 @@
#include "internal.h"
#include <algorithm>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "external/hash-library/sha256.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/prettywriter.h"
#include "external/rapidjson/stringbuffer.h"
#include "util/fileutils.h"
#include "util/logging.h"
using namespace rapidjson;
namespace patcher {
bool is_game_id_wildcard_matched(const std::string& id_from_config) {
return ((id_from_config.compare(0, 3, avs::game::MODEL) == 0) &&
(id_from_config.compare(10, 10, avs::game::EXT) == 0));
}
void config_load() {
log_info("patchmanager", "loading config");
// read config file
std::string config = fileutils::text_read(config_path);
if (!config.empty()) {
// parse document
Document doc;
doc.Parse(config.c_str());
// check parse error
auto error = doc.GetParseError();
if (error) {
log_warning("patchmanager", "config file parse error: {}", static_cast<uint32_t>(error));
}
// verify root is a dict
if (doc.IsObject()) {
// read auto apply settings
auto auto_apply = doc.FindMember("auto_apply");
if (auto_apply != doc.MemberEnd() && auto_apply->value.IsArray()) {
// get game id
const auto game_id = avs::game::get_identifier();
// iterate entries
setting_auto_apply = false;
setting_auto_apply_list.clear();
for (auto &entry : auto_apply->value.GetArray()) {
if (entry.IsString()) {
// check if this is our game identifier
const std::string entry_id = entry.GetString();
if (!setting_auto_apply) {
if (game_id == entry_id) {
// exact match
setting_auto_apply = true;
log_misc(
"patchmanager",
"matched auto apply entry by full game identifier: {}",
entry_id);
} else if (is_game_id_wildcard_matched(entry_id)) {
// match on model and ext, ignoring dest/spec/rev
// sample: LDJ:J:E:A:2025011400
setting_auto_apply = true;
log_misc(
"patchmanager",
"matched auto apply entry by partial game identifier: {}:?:?:?:{}",
avs::game::MODEL, avs::game::EXT);
}
}
// move to list
setting_auto_apply_list.emplace_back(entry_id);
}
}
if (!setting_auto_apply) {
log_misc(
"patchmanager",
"no auto apply entry matched, patches will not load automatically");
}
}
// read enabled patches
auto patches_enabled = doc.FindMember("patches_enabled");
if (patches_enabled != doc.MemberEnd() && patches_enabled->value.IsArray()) {
setting_patches_enabled.clear();
for (const auto &patch : patches_enabled->value.GetArray()) {
if (patch.IsString()) {
setting_patches_enabled.emplace_back(std::string(patch.GetString()));
}
}
}
// read enabled union patches
auto patches_union_enabled = doc.FindMember("union_patches_enabled");
if (patches_union_enabled != doc.MemberEnd() && patches_union_enabled->value.IsObject()) {
setting_union_patches_enabled.clear();
for (auto it = patches_union_enabled->value.MemberBegin(); it != patches_union_enabled->value.MemberEnd(); ++it) {
if (it->name.IsString() && it->value.IsString()) {
setting_union_patches_enabled[it->name.GetString()] = it->value.GetString();
}
}
}
// read enabled integer patches
auto patches_int_enabled = doc.FindMember("integer_patches_enabled");
if (patches_int_enabled != doc.MemberEnd() && patches_int_enabled->value.IsObject()) {
setting_int_patches_enabled.clear();
for (auto it = patches_int_enabled->value.MemberBegin(); it != patches_int_enabled->value.MemberEnd(); ++it) {
if (it->name.IsString() && it->value.IsNumber()) {
setting_int_patches_enabled[it->name.GetString()] = it->value.GetInt();
}
}
}
// read remote patch URLs
auto remote_url_history = doc.FindMember("remote_url_history");
if (remote_url_history != doc.MemberEnd() && remote_url_history->value.IsArray()) {
url_recents.clear();
for (const auto &url : remote_url_history->value.GetArray()) {
if (url.IsString()) {
url_recents.emplace_back(std::string(url.GetString()));
}
}
}
}
}
}
std::string patch_hash(PatchData &patch) {
SHA256 hash;
hash.add(patch.game_code.c_str(), patch.game_code.length());
if (patch.datecode_min != 0 || patch.datecode_max != 0) {
hash.add(&patch.datecode_min, sizeof(patch.datecode_min));
hash.add(&patch.datecode_max, sizeof(patch.datecode_max));
}
if (!patch.peIdentifier.empty()) {
hash.add(patch.peIdentifier.c_str(), patch.peIdentifier.length());
}
hash.add(patch.name.c_str(), patch.name.length());
hash.add(patch.description.c_str(), patch.description.length());
return hash.getHash();
}
void config_save() {
// create document
Document doc;
doc.Parse(
"{"
" \"auto_apply\": [],"
" \"patches_enabled\": [],"
" \"union_patches_enabled\": {},"
" \"integer_patches_enabled\": {},"
" \"remote_url_history\": []"
"}"
);
// check parse error
auto error = doc.GetParseError();
if (error) {
log_warning("patchmanager", "template parse error: {}", static_cast<uint32_t>(error));
}
// auto apply setting
auto &auto_apply_list = doc["auto_apply"];
auto game_id = avs::game::get_identifier();
bool game_id_added = false;
for (auto &entry : setting_auto_apply_list) {
if (entry == game_id || is_game_id_wildcard_matched(entry)) {
if (!setting_auto_apply) {
continue;
}
game_id_added = true;
}
auto_apply_list.PushBack(StringRef(entry.c_str()), doc.GetAllocator());
}
if (setting_auto_apply && !game_id_added) {
auto_apply_list.PushBack(StringRef(game_id.c_str()), doc.GetAllocator());
}
// get enabled patches
auto &doc_patches_enabled = doc["patches_enabled"];
auto &doc_union_patches_enable = doc["union_patches_enabled"];
auto &doc_int_patches_enable = doc["integer_patches_enabled"];
for (auto &patch : patches) {
auto hash = patch_hash(patch);
if (patch.type == PatchType::Union) {
// enable hash if known as enabled, overridden and missing from list
if (patch.enabled) {
setting_union_patches_enabled[hash] = patch.selected_union_name;
} else {
setting_union_patches_enabled.erase(hash);
}
} else if (patch.type == PatchType::Integer) {
if (patch.enabled) {
setting_int_patches_enabled[hash] = patch.patch_number.value;
} else {
setting_int_patches_enabled.erase(hash);
}
} else {
// hash patch and find entry
auto entry = std::find(setting_patches_enabled.begin(), setting_patches_enabled.end(), hash);
// enable hash if known as enabled, overridden and missing from list
if ((patch.last_status == PatchStatus::Enabled && patch.enabled)
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && patch.enabled)) {
if (entry == setting_patches_enabled.end()) {
setting_patches_enabled.emplace_back(hash);
}
}
// disable hash if patch known as disabled
if (patch.last_status == PatchStatus::Disabled
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && !patch.enabled)) {
if (entry != setting_patches_enabled.end()) {
setting_patches_enabled.erase(entry);
}
}
}
}
// add hashes to document
for (auto &hash : setting_patches_enabled) {
Value hash_value(hash.c_str(), doc.GetAllocator());
doc_patches_enabled.PushBack(hash_value, doc.GetAllocator());
}
for (auto& it : setting_union_patches_enabled) {
const std::string& key = it.first;
const std::string& val = it.second;
doc_union_patches_enable.AddMember(StringRef(key.c_str()), StringRef(val.c_str()), doc.GetAllocator());
}
for (auto& it : setting_int_patches_enabled) {
const std::string& key = it.first;
const int32_t& val = it.second;
doc_int_patches_enable.AddMember(StringRef(key.c_str()), val, doc.GetAllocator());
}
// remote URLs
auto &doc_url_history = doc["remote_url_history"];
for (auto& url : url_recents) {
Value url_value(url.c_str(), doc.GetAllocator());
doc_url_history.PushBack(url_value, doc.GetAllocator());
}
// build JSON
StringBuffer buffer;
PrettyWriter<StringBuffer> writer(buffer);
doc.Accept(writer);
// save to file
if (fileutils::write_config_file("patchmanager", config_path, buffer.GetString())) {
config_dirty = false;
} else {
log_warning("patchmanager", "unable to save config file");
}
}
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <map>
#include <utility>
#include "patch_manager.h"
#include "external/rapidjson/document.h"
#include "util/nt_loader.h"
namespace patcher {
// internal engine state (patcher module only)
extern std::vector<std::string> setting_auto_apply_list;
extern std::vector<std::string> setting_patches_enabled;
extern std::map<std::string, std::string> setting_union_patches_enabled;
extern std::map<std::string, int64_t> setting_int_patches_enabled;
extern std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
extern std::filesystem::path LOCAL_PATCHES_PATH;
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
extern bool ldr_registered;
extern void *ldr_notify_cookie;
extern std::vector<std::string> ldr_target_libraries;
// internal helpers
void append_patches(
std::string& patches_json,
bool apply_patches = false,
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
std::string pe_identifier_for_patch = "");
bool is_game_id_wildcard_matched(const std::string& id_from_config);
std::string getFromUrl(const std::string& dll_name, const std::string& url);
bool load_from_patches_json(bool apply_patches);
void load_embedded_patches(bool apply_patches);
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
bool is_patch_group_definition(const rapidjson::Value& patch);
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
const rapidjson::Document& doc);
std::string resolve_patch_group_id(
const rapidjson::Value& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
const std::string& game_code,
const char *patch_name);
void register_patch_group(
PatchData& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions);
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
std::string patch_hash(PatchData& patch);
void clear_dll_maps();
int64_t parse_little_endian_int(uint8_t *bytes, size_t size);
void int_to_little_endian_bytes(int64_t value, uint8_t *bytes, size_t size);
std::vector<uint8_t> *find_in_dll_map(
const std::string& dll_name, size_t offset, size_t size);
std::vector<uint8_t> *find_in_dll_map_org(
const std::string& dll_name, size_t offset, size_t size);
bool restore_bytes_from_dll_map_org(
uint8_t *destination, const std::string& dll_name, size_t offset, size_t size);
void create_dll_backup(
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
std::string fix_up_dll_name(const std::string& dll_name);
uint8_t *get_dll_offset_for_patch_apply(
const std::string& dll_name, uint64_t data_offset, size_t size_in_bytes);
uint64_t parse_json_data_offset(
const std::string& patch_name, const rapidjson::Value& value);
void print_auto_apply_status(PatchData& patch);
}
+120
View File
@@ -0,0 +1,120 @@
#include "internal.h"
#include <algorithm>
#include <windows.h>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "util/fileutils.h"
#include "util/logging.h"
#include "util/utils.h"
namespace patcher {
std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
std::filesystem::path config_path;
bool config_dirty = false;
bool setting_auto_apply = false;
std::vector<std::string> setting_auto_apply_list;
std::vector<std::string> setting_patches_enabled;
std::map<std::string, std::string> setting_union_patches_enabled;
std::map<std::string, int64_t> setting_int_patches_enabled;
std::string patch_url;
std::filesystem::path LOCAL_PATCHES_PATH("patches");
std::string ACTIVE_JSON_FILE;
std::vector<PatchData> patches;
bool local_patches_initialized = false;
std::vector<size_t> patches_sorted;
std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
{"arkmdxp3.dll", {"gamemdx.dll"}},
{"arkmdxp4.dll", {"gamemdx.dll"}},
{"arkmdxbio2.dll", {"gamemdx.dll"}},
{"arkndd.dll", {"gamendd.dll"}},
{"arkkep.dll", {"game.dll"}},
{"arkjc9.dll", {"gamejc9.dll"}},
{"arkkdm.dll", {"gamekdm.dll"}},
{"arkmmd.dll", {"gamemmd.dll"}},
{"arkklp.dll", {"lpac.dll"}},
{"arknck.dll", {"weac.dll"}},
{"gdxg.dll", {"game.dll", "libshare-pj.dll", "boot.dll"}}
};
std::string url_fetch_errors;
size_t url_recent_idx = -1;
std::vector<std::string> url_recents;
bool ldr_registered = false;
void *ldr_notify_cookie = nullptr;
std::vector<std::string> ldr_target_libraries;
void init() {
if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) {
config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value();
log_info("patchmanager", "using custom config file path: {}", config_path);
} else {
config_path =
fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json");
}
// register for DLL load notifications so patches can be (re)applied as
// the game's target libraries come into memory. registration happens
// once and is left in place for the lifetime of the process.
if (!ldr_registered) {
ldr_target_libraries = getExtraDlls(avs::game::DLL_NAME);
ldr_target_libraries.push_back(avs::game::DLL_NAME);
const auto register_fn = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
if (register_fn && NT_SUCCESS(register_fn(
0, &loader_notification, nullptr, &ldr_notify_cookie))) {
log_info("patchmanager", "registered for DLL load notifications");
} else {
log_warning("patchmanager", "failed to register for DLL load notifications");
}
ldr_registered = true;
}
if (!local_patches_initialized) {
patch_url.clear();
if (fileutils::file_exists(config_path)) {
config_load();
}
if (cfg::CONFIGURATOR_STANDALONE) {
reload_local_patches(true);
}
}
}
void apply_patches_on_start() {
if (!local_patches_initialized) {
reload_local_patches(true);
}
}
VOID CALLBACK loader_notification(
ULONG reason,
PCLDR_DLL_NOTIFICATION_DATA data,
PVOID) {
if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
const auto dll = strtolower(std::filesystem::path({
data->Loaded.FullDllName->Buffer,
data->Loaded.FullDllName->Length / sizeof(wchar_t)
}).filename().string());
if (std::ranges::find(ldr_target_libraries, dll) != ldr_target_libraries.end()) {
reload_local_patches(true);
}
}
}
}
File diff suppressed because it is too large Load Diff
+179
View File
@@ -0,0 +1,179 @@
#include "internal.h"
#include <cstring>
#include "util/logging.h"
#include "util/utils.h"
namespace patcher {
static std::pair<std::string, std::string> make_patch_group_key(
const std::string& game_code,
const std::string& group_id) {
return {strtolower(game_code), group_id};
}
static bool has_embedded_null(const rapidjson::Value& value) {
return strlen(value.GetString()) != value.GetStringLength();
}
bool is_patch_group_definition(const rapidjson::Value& patch) {
if (!patch.IsObject()) {
return false;
}
const auto type_it = patch.FindMember("type");
return type_it != patch.MemberEnd()
&& type_it->value.IsString()
&& type_it->value.GetStringLength() == strlen("group")
&& !_stricmp(type_it->value.GetString(), "group");
}
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
const rapidjson::Document& doc) {
std::map<std::pair<std::string, std::string>, PatchGroup> groups;
for (const auto& patch : doc.GetArray()) {
if (!is_patch_group_definition(patch)) {
continue;
}
const auto id_it = patch.FindMember("id");
const auto game_code_it = patch.FindMember("gameCode");
const auto name_it = patch.FindMember("name");
if (id_it == patch.MemberEnd() || !id_it->value.IsString()
|| id_it->value.GetStringLength() == 0
|| has_embedded_null(id_it->value)
|| game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()
|| game_code_it->value.GetStringLength() == 0
|| has_embedded_null(game_code_it->value)
|| name_it == patch.MemberEnd() || !name_it->value.IsString()
|| name_it->value.GetStringLength() == 0
|| has_embedded_null(name_it->value)) {
log_warning("patchmanager", "invalid patch group definition");
continue;
}
PatchGroup group;
group.name.assign(name_it->value.GetString(), name_it->value.GetStringLength());
group.name_in_lower_case = strtolower(group.name);
const std::string group_id(
id_it->value.GetString(),
id_it->value.GetStringLength());
const auto description_it = patch.FindMember("description");
if (description_it != patch.MemberEnd()) {
if (!description_it->value.IsString()
|| has_embedded_null(description_it->value)) {
log_warning("patchmanager", "invalid description for patch group {}", group_id);
continue;
}
group.description.assign(
description_it->value.GetString(),
description_it->value.GetStringLength());
}
const auto caution_it = patch.FindMember("caution");
if (caution_it != patch.MemberEnd()) {
if (!caution_it->value.IsString() || has_embedded_null(caution_it->value)) {
log_warning("patchmanager", "invalid caution for patch group {}", group_id);
continue;
}
group.caution.assign(
caution_it->value.GetString(),
caution_it->value.GetStringLength());
}
const std::string game_code(
game_code_it->value.GetString(),
game_code_it->value.GetStringLength());
if (!groups.emplace(
make_patch_group_key(game_code, group_id),
std::move(group)).second) {
log_warning(
"patchmanager",
"duplicate patch group definition for {}/{}, ignoring duplicate",
game_code,
group_id);
}
}
return groups;
}
static const PatchGroup* find_patch_group(
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
const std::string& game_code,
const std::string& group_id) {
const auto group = groups.find(make_patch_group_key(game_code, group_id));
return group == groups.end() ? nullptr : &group->second;
}
const PatchGroup* find_patch_group(const PatchData& patch) {
return find_patch_group(patch_groups, patch.game_code, patch.group_id);
}
std::string resolve_patch_group_id(
const rapidjson::Value& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
const std::string& game_code,
const char *patch_name) {
const auto group_it = patch.FindMember("group");
if (group_it == patch.MemberEnd()) {
return "";
}
if (!group_it->value.IsString()
|| group_it->value.GetStringLength() == 0
|| has_embedded_null(group_it->value)) {
log_warning("patchmanager", "invalid group reference for {}", patch_name);
return "";
}
const std::string group_id(
group_it->value.GetString(),
group_it->value.GetStringLength());
if (!find_patch_group(groups, game_code, group_id)) {
log_warning(
"patchmanager",
"unknown patch group {}/{} referenced by {}",
game_code,
group_id,
patch_name);
return "";
}
return group_id;
}
void register_patch_group(
PatchData& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions) {
if (patch.group_id.empty()) {
return;
}
const auto *definition = find_patch_group(
definitions,
patch.game_code,
patch.group_id);
if (!definition) {
patch.group_id.clear();
return;
}
const auto key = make_patch_group_key(patch.game_code, patch.group_id);
const auto [existing, inserted] = patch_groups.emplace(key, *definition);
if (!inserted
&& (existing->second.name != definition->name
|| existing->second.description != definition->description
|| existing->second.caution != definition->caution)) {
log_warning(
"patchmanager",
"conflicting group metadata for {}/{}, ignoring group on {}",
patch.game_code,
patch.group_id,
patch.name);
patch.group_id.clear();
}
}
}
+142
View File
@@ -0,0 +1,142 @@
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace patcher {
enum class PatchType {
Unknown,
Memory,
Signature,
Union,
Integer,
};
enum class PatchStatus {
Error,
Disabled,
Enabled,
};
enum class PatchUrlStatus {
Valid,
Invalid,
Unapplied,
ValidButNoData,
Partial,
};
struct MemoryPatch {
std::string dll_name = "";
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
size_t data_disabled_len = 0;
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
size_t data_enabled_len = 0;
uint64_t data_offset = 0;
uint8_t *data_offset_ptr = nullptr;
bool fatal_error = false;
};
struct PatchData;
struct SignaturePatch {
std::string dll_name = "";
std::string signature = "", replacement = "";
uint64_t offset = 0;
int64_t usage = 0;
MemoryPatch to_memory(PatchData *patch);
};
struct UnionPatch {
std::string name = "";
std::string dll_name = "";
std::shared_ptr<uint8_t[]> data = nullptr;
size_t data_len = 0;
uint64_t offset = 0;
uint8_t *data_offset_ptr = nullptr;
bool fatal_error = false;
};
struct NumberPatch {
std::string dll_name = "";
uint64_t data_offset = 0;
uint8_t *data_offset_ptr = nullptr;
int32_t min;
int32_t max;
int32_t value;
size_t size_in_bytes;
bool fatal_error = false;
};
struct PatchGroup {
std::string name;
std::string description;
std::string caution;
std::string name_in_lower_case;
};
struct PatchData {
bool enabled;
std::string game_code;
int datecode_min = 0;
int datecode_max = 0;
std::string name, description, caution;
std::string name_in_lower_case = "";
PatchType type;
bool preset;
std::vector<MemoryPatch> patches_memory;
std::vector<UnionPatch> patches_union;
NumberPatch patch_number;
std::string group_id;
PatchStatus last_status;
std::string hash;
bool unverified = false;
std::string peIdentifier;
std::string error_reason = "";
// for union patch only
std::string selected_union_name = "";
};
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info = false);
// lifecycle: init() sets up the config path and registers for DLL-load
// notifications; apply_patches_on_start() applies auto-apply patches once the
// game DLL is loaded. both are safe to call repeatedly.
void init();
void apply_patches_on_start();
// patch load / apply operations (also driven by the overlay window)
void reload_local_patches(bool apply_patches = false);
bool import_remote_patches_to_disk();
void hard_apply_patches();
void config_load();
void config_save();
std::vector<std::string> getExtraDlls(const std::string& firstDll);
// process-wide patch engine state, rendered and edited by the overlay window
extern std::filesystem::path config_path;
extern bool config_dirty;
extern bool setting_auto_apply;
extern std::string patch_url;
extern std::string ACTIVE_JSON_FILE;
extern std::vector<PatchData> patches;
extern bool local_patches_initialized;
extern std::vector<size_t> patches_sorted;
extern std::string url_fetch_errors;
extern size_t url_recent_idx;
extern std::vector<std::string> url_recents;
PatchStatus is_patch_active(PatchData& patch);
bool apply_patch(PatchData& patch, bool active);
const PatchGroup* find_patch_group(const PatchData& patch);
std::string displayPath(const std::filesystem::path& path);
}
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -197,8 +197,8 @@ namespace rawinput {
HANDLE handle = INVALID_HANDLE_VALUE;
DeviceType type = UNKNOWN;
DeviceInfo info;
std::mutex *mutex;
std::mutex *mutex_out;
std::mutex *mutex = nullptr;
std::mutex *mutex_out = nullptr;
bool updated = true;
bool output_pending = true;
bool output_enabled = false;
@@ -213,5 +213,7 @@ namespace rawinput {
double input_time = 0.0;
double input_hz = 0.f;
double input_hz_max = 0.f;
// when adding or removing a field, update replace_device_slot to match
};
}
+40 -17
View File
@@ -4,6 +4,10 @@
#include <dbt.h>
#include <devguid.h>
#include <hidclass.h>
#include <ntddkbd.h>
#include <ntddmou.h>
#include <objbase.h>
#include "misc/eamuse.h"
#include "rawinput.h"
@@ -12,31 +16,42 @@
namespace rawinput {
DEFINE_GUID(GUID_HID, 0x4D1E55B2L, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30);
static bool is_rawinput_interface(const GUID &guid) {
return IsEqualGUID(guid, GUID_DEVINTERFACE_HID)
|| IsEqualGUID(guid, GUID_DEVINTERFACE_KEYBOARD)
|| IsEqualGUID(guid, GUID_DEVINTERFACE_MOUSE);
}
HotplugManager::HotplugManager(RawInputManager *ri_mgr, HWND hWnd) : ri_mgr(ri_mgr) {
auto register_notification = [hWnd](const GUID &guid, const char *name) -> HANDLE {
DEV_BROADCAST_DEVICEINTERFACE settings {
.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE),
.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
.dbcc_reserved = 0,
.dbcc_classguid = guid,
.dbcc_name = {0},
};
// init settings
DEV_BROADCAST_DEVICEINTERFACE settings_hid {
.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE),
.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
.dbcc_reserved = 0,
.dbcc_classguid = GUID_HID,
.dbcc_name = {0},
auto notification = RegisterDeviceNotification(hWnd, &settings, DEVICE_NOTIFY_WINDOW_HANDLE);
if (notification == nullptr) {
log_warning("hotplug", "failed to register {} notifications: {}", name, GetLastError());
}
return notification;
};
// register notifications
this->hotplug_hid = RegisterDeviceNotification(hWnd, &settings_hid, DEVICE_NOTIFY_WINDOW_HANDLE);
if (this->hotplug_hid == nullptr) {
log_warning("hotplug", "failed to register HID notifications: {}", GetLastError());
}
this->hotplug_hid = register_notification(GUID_DEVINTERFACE_HID, "HID");
this->hotplug_keyboard = register_notification(GUID_DEVINTERFACE_KEYBOARD, "keyboard");
this->hotplug_mouse = register_notification(GUID_DEVINTERFACE_MOUSE, "mouse");
}
HotplugManager::~HotplugManager() {
// unregister notifications
if (this->hotplug_hid != nullptr) {
UnregisterDeviceNotification(this->hotplug_hid);
for (auto notification : {this->hotplug_hid, this->hotplug_keyboard, this->hotplug_mouse}) {
if (notification != nullptr) {
UnregisterDeviceNotification(notification);
}
}
}
@@ -54,11 +69,16 @@ namespace rawinput {
switch (hdr->dbch_devicetype) {
case DBT_DEVTYP_DEVICEINTERFACE: {
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
this->ri_mgr->midi_scan_start();
// check if class is not HID
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
// check if this is one of the registered RawInput interfaces
if (!is_rawinput_interface(dev->dbcc_classguid)) {
break;
}
// keyboard and mouse class events accompany the HID event, so scan MIDI only once
if (IsEqualGUID(dev->dbcc_classguid, GUID_DEVINTERFACE_HID)) {
this->ri_mgr->midi_scan_start();
}
// hotplug
std::string name(dev->dbcc_name);
@@ -109,6 +129,9 @@ namespace rawinput {
switch (hdr->dbch_devicetype) {
case DBT_DEVTYP_DEVICEINTERFACE: {
auto dev = (DEV_BROADCAST_DEVICEINTERFACE*) hdr;
if (!is_rawinput_interface(dev->dbcc_classguid)) {
break;
}
std::string name(dev->dbcc_name);
// destruct device
+3 -1
View File
@@ -9,7 +9,9 @@ namespace rawinput {
class HotplugManager {
private:
RawInputManager *ri_mgr;
HANDLE hotplug_hid;
HANDLE hotplug_hid = nullptr;
HANDLE hotplug_keyboard = nullptr;
HANDLE hotplug_mouse = nullptr;
public:
HotplugManager(RawInputManager *ri_mgr, HWND hwnd);
+4 -5
View File
@@ -238,8 +238,6 @@ void rawinput::RawInputManager::devices_scan_midi() {
midi_device.name = midi_identifier;
midi_device.desc = to_string(midi_device_caps.szPname);
midi_device.info = midi_device_info;
midi_device.mutex = new std::mutex();
midi_device.mutex_out = new std::mutex();
midi_device.midiInfo = midi_device_midi_info;
// mutate the shared device list under lock (the slow WinMM calls above
@@ -256,10 +254,9 @@ void rawinput::RawInputManager::devices_scan_midi() {
// carry over ID
midi_device.id = device.id;
// destruct and replace, reusing the slot's existing mutexes
// destruct and replace the slot in place under its locks
this->devices_destruct(&device);
reuse_device_mutexes(midi_device, device);
device = midi_device;
replace_device_slot(device, midi_device);
// notify change
for (auto &cb : this->callback_change) {
@@ -275,6 +272,8 @@ void rawinput::RawInputManager::devices_scan_midi() {
}
// add device to list
midi_device.mutex = new std::mutex();
midi_device.mutex_out = new std::mutex();
auto &device = this->devices.emplace_back(midi_device);
// notify add
+66 -56
View File
@@ -44,18 +44,40 @@ namespace rawinput {
bool OS_WINDOW_ACTIVE = false;
}
// when replacing a device slot in place, keep the old slot's per-device mutexes
// instead of the freshly allocated pair on `replacement`. their addresses stay
// stable, so a thread still holding a snapshot pointer to the slot (e.g. the
// output thread blocked on mutex_out, which is taken without devices_mutex)
// never locks freed memory. the freshly allocated pair is freed here rather
// than leaking the old one. the slot must already be destructed so both
// mutexes are unlocked
void rawinput::RawInputManager::reuse_device_mutexes(Device &replacement, const Device &existing) {
delete replacement.mutex;
delete replacement.mutex_out;
replacement.mutex = existing.mutex;
replacement.mutex_out = existing.mutex_out;
// serialize replacement with input and output users, matching teardown lock order
void rawinput::RawInputManager::replace_device_slot(Device &existing, Device &replacement) {
if (existing.mutex == nullptr || existing.mutex_out == nullptr) {
log_fatal("rawinput", "existing device slot has no mutexes");
}
if (replacement.mutex != nullptr || replacement.mutex_out != nullptr) {
log_fatal("rawinput", "replacement device already has mutexes");
}
std::lock_guard<std::mutex> lock_out(*existing.mutex_out);
std::lock_guard<std::mutex> lock(*existing.mutex);
// copy every field except the stable mutex pointers: snapshot users may be blocked
// on those exact locks without devices_mutex, so rewriting the pointers could send a
// waiter to a different mutex. when adding a field, add it here too (see device.h)
existing.id = replacement.id;
existing.name = std::move(replacement.name);
existing.desc = std::move(replacement.desc);
existing.handle = replacement.handle;
existing.type = replacement.type;
existing.info = replacement.info;
existing.updated = replacement.updated;
existing.output_pending = replacement.output_pending;
existing.output_enabled = replacement.output_enabled;
existing.mouseInfo = replacement.mouseInfo;
existing.keyboardInfo = replacement.keyboardInfo;
existing.hidInfo = replacement.hidInfo;
existing.midiInfo = replacement.midiInfo;
existing.sextetInfo = replacement.sextetInfo;
existing.piuioDev = replacement.piuioDev;
existing.smxstageInfo = replacement.smxstageInfo;
existing.smxdedicabInfo = replacement.smxdedicabInfo;
existing.input_time = replacement.input_time;
existing.input_hz = replacement.input_hz;
existing.input_hz_max = replacement.input_hz_max;
}
rawinput::RawInputManager::RawInputManager() {
@@ -224,7 +246,8 @@ void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_
return;
}
// iterate devices
// hotplug gives us an interface path rather than a RawInput handle, so find its
// matching entry; an empty path means this is a full device scan
for (UINT device_cur_index = 0; device_cur_index < device_no; device_cur_index++) {
auto device = &device_list.get()[device_cur_index];
if (device_name.length() == 0) {
@@ -277,8 +300,6 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
new_device.name = device_name;
new_device.desc = device_description;
new_device.info = device_info;
new_device.mutex = new std::mutex();
new_device.mutex_out = new std::mutex();
new_device.input_time = get_performance_seconds();
switch (device->dwType) {
case RIM_TYPEMOUSE:
@@ -805,10 +826,10 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
// carry over old device ID
new_device.id = prev_device.id;
// destruct and replace, reusing the slot's existing mutexes
// destruct and replace the slot in place under its locks
this->devices_destruct(&prev_device);
reuse_device_mutexes(new_device, prev_device);
prev_device = new_device;
replace_device_slot(prev_device, new_device);
this->rawinput_handles.add(&prev_device);
// notify change
for (auto &cb : this->callback_change) {
@@ -820,7 +841,10 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
}
// add device to list
new_device.mutex = new std::mutex();
new_device.mutex_out = new std::mutex();
auto &added_device = this->devices.emplace_back(new_device);
this->rawinput_handles.add(&added_device);
if (log) {
log_info("rawinput", "added device: {} / {}", added_device.desc, added_device.name);
}
@@ -951,8 +975,6 @@ void rawinput::RawInputManager::devices_scan_xinput() {
device.name = xinput::get_device_desc(player);
device.desc = fmt::format("XInput Gamepad P{}", player + 1);
device.handle = reinterpret_cast<HANDLE>(player);
device.mutex = new std::mutex();
device.mutex_out = new std::mutex();
return device;
};
@@ -969,11 +991,10 @@ void rawinput::RawInputManager::devices_scan_xinput() {
log_info("rawinput", "overwriting previously destroyed XInput device: {}", prev_device.name);
const auto old_id = prev_device.id;
// replace in place, reusing the slot's existing mutexes
// replace the slot in place under its locks
auto replacement = create_device(player);
reuse_device_mutexes(replacement, prev_device);
prev_device = replacement;
prev_device.id = old_id;
replacement.id = old_id;
replace_device_slot(prev_device, replacement);
// notify change
for (auto &cb : this->callback_change) {
@@ -989,6 +1010,8 @@ void rawinput::RawInputManager::devices_scan_xinput() {
log_info("rawinput", "adding new XInput device: player {}", player + 1);
auto new_xinput_device = create_device(player);
new_xinput_device.id = this->devices.size() + 1;
new_xinput_device.mutex = new std::mutex();
new_xinput_device.mutex_out = new std::mutex();
auto &device = this->devices.emplace_back(new_xinput_device);
// notify add
@@ -1438,6 +1461,8 @@ void rawinput::RawInputManager::devices_destruct() {
void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
this->rawinput_handles.remove(device);
// check if destroyed
if (device->type == DESTROYED) {
return;
@@ -1454,21 +1479,20 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
// devices_mutex, so mutex_out is what actually serializes it against us
std::lock_guard<std::mutex> lock_out(*device->mutex_out);
// mark as destroyed
auto device_type = device->type;
device->type = DESTROYED;
// mark the device destroyed while synchronized with in-flight input
DeviceType device_type;
{
std::lock_guard<std::mutex> lock(*device->mutex);
device_type = device->type;
device->type = DESTROYED;
}
// notify change
// callbacks may lock the device mutex
for (auto &cb : this->callback_change) {
cb.f(cb.data, device);
}
/*
* lock device
* note: this is an exception to only locking devices when we acquire them from the list
* callbacks could want to lock the mutex as well and it isn't recursive
* this also means the device must be unlocked before calling this function
*/
// wait for input users before releasing device resources
std::lock_guard<std::mutex> lock(*device->mutex);
// close device handles
@@ -1561,21 +1585,13 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
// find device
HANDLE device_handle = data->header.hDevice;
// lock the device list so a concurrent scan can't mutate it while we iterate
std::lock_guard<std::recursive_mutex> devices_lock(ref->devices_mutex);
for (auto &device : ref->devices_get()) {
// skip if this is the wrong device
if (device.handle != device_handle) {
continue;
}
auto acquired_device = ref->rawinput_handles.acquire(device_handle);
if (acquired_device.device != nullptr) {
auto &device = *acquired_device.device;
// get input time
const auto input_time = get_performance_seconds();
// lock device
device.mutex->lock();
// update hz
double diff_time = input_time - device.input_time;
if (diff_time > 0.0001) {
@@ -1834,7 +1850,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
USAGE usage = usages[usage_num] - button_caps.Range.UsageMin;
// guard against some buggy device sending an event for a usage below `UsageMin`
// guard against some buggy device sending an event for a usage below UsageMin
if (usage < button_count) {
new_states[usage] = true;
}
@@ -1937,11 +1953,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
break;
}
// free device
device.mutex->unlock();
// don't iterate through the other devices
break;
acquired_device.lock.unlock();
}
// update controller state ring buffers (DDR/MDXF)
@@ -1973,6 +1985,9 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
void rawinput::RawInputManager::device_write_output(Device *device, bool only_updated) {
// lock device
std::lock_guard<std::mutex> lock(*device->mutex_out);
// check if output is enabled
if (!device->output_enabled) {
return;
@@ -1983,9 +1998,6 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
return;
}
// lock device
device->mutex_out->lock();
// mark device as updated
device->output_pending = false;
@@ -2208,8 +2220,6 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
break;
}
// unlock device
device->mutex_out->unlock();
}
void rawinput::RawInputManager::devices_flush_output(bool optimized) {
+8 -4
View File
@@ -12,6 +12,7 @@
#include "device.h"
#include "hotplug.h"
#include "rawinput_handles.h"
#include "rawinput/xinput.h"
#include "util/scope_guard.h"
@@ -74,6 +75,9 @@ namespace rawinput {
std::list<Device> devices;
std::recursive_mutex devices_mutex;
// separate lookup isolated from devices_mutex for the latency-sensitive WM_INPUT path
RawInputHandles rawinput_handles;
WNDCLASSEX input_hwnd_class {};
std::thread *input_thread = nullptr;
std::thread *midi_thread = nullptr;
@@ -134,10 +138,9 @@ namespace rawinput {
static void CALLBACK input_midi_proc(HMIDIIN, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
static DeviceInfo get_device_info(const std::string &device_name);
// shared by the rawinput / xinput / midi device scans when replacing a slot
// in place; keeps the existing slot's mutex pair so snapshot pointers held by
// other threads stay valid (see the definition in rawinput.cpp)
static void reuse_device_mutexes(Device &replacement, const Device &existing);
// replace an already-destructed slot in place, holding both device locks so
// concurrent pollers and the output thread never see a half-copied Device
static void replace_device_slot(Device &existing, Device &replacement);
public:
@@ -193,6 +196,7 @@ namespace rawinput {
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
for (auto &device : devices_get()) {
if (device.type == MIDI) {
std::lock_guard<std::mutex> device_lock(*device.mutex);
device.midiInfo->freeze = freeze;
if (!freeze) {
for (unsigned short index = 0; index < device.midiInfo->states.size(); index++) {
+31
View File
@@ -0,0 +1,31 @@
#include "rawinput_handles.h"
void rawinput::RawInputHandles::add(Device *device) {
std::lock_guard<std::mutex> lock(this->mutex);
this->devices[device->handle] = device;
}
void rawinput::RawInputHandles::remove(Device *device) {
std::lock_guard<std::mutex> lock(this->mutex);
// teardown is shared by every device type, and handles can be reused; only erase
// an entry that still belongs to this exact RawInput device
auto it = this->devices.find(device->handle);
if (it != this->devices.end() && it->second == device) {
this->devices.erase(it);
}
}
rawinput::RawInputHandles::AcquiredDevice rawinput::RawInputHandles::acquire(HANDLE handle) {
std::lock_guard<std::mutex> index_lock(this->mutex);
auto it = this->devices.find(handle);
if (it == this->devices.end()) {
return {};
}
auto *device = it->second;
// take the device mutex while index_lock is still held so teardown can't free it
// between the lookup and the lock
return {device, std::unique_lock<std::mutex>(*device->mutex)};
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <mutex>
#include <unordered_map>
#include <windows.h>
#include "device.h"
namespace rawinput {
class RawInputHandles {
public:
struct AcquiredDevice {
Device *device = nullptr;
std::unique_lock<std::mutex> lock;
};
void add(Device *device);
void remove(Device *device);
AcquiredDevice acquire(HANDLE handle);
private:
// non-owning index; RawInputManager owns the stable device slots
std::unordered_map<HANDLE, Device *> devices;
std::mutex mutex;
};
}
+21 -13
View File
@@ -35,6 +35,8 @@ namespace rawinput::touch {
long DISPLAY_NATIVE_Y = 0L;
bool DISPLAY_INITIALIZED = false;
static void update_current_display_mode();
bool aspect_compensation_enabled() {
switch (ASPECT_COMPENSATION_MODE) {
case AspectMode::On:
@@ -279,7 +281,7 @@ namespace rawinput::touch {
// check if display is initialized
if (!DISPLAY_INITIALIZED) {
display_update();
update_current_display_mode();
}
// update timeouts here as well so events are in the right order
@@ -638,12 +640,7 @@ namespace rawinput::touch {
return false;
}
void display_update() {
// check if disabled
if (DISABLED)
return;
static void update_current_display_mode() {
// determine monitor size
static RECT display_rect;
GetWindowRect(GetDesktopWindow(), &display_rect);
@@ -651,12 +648,6 @@ namespace rawinput::touch {
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
log_info("rawinput", "primary display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
// determine the native (maximum) resolution of the primary display; used to
// detect and undo letterboxing when a non-native display mode is in use
detect_native_resolution();
log_info("rawinput", "primary display native size: {}x{}",
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
// determine monitor orientation
DEVMODE display_mode{};
display_mode.dmSize = sizeof(DEVMODE);
@@ -703,4 +694,21 @@ namespace rawinput::touch {
// mark as initialized
DISPLAY_INITIALIZED = true;
}
void display_update() {
// check if disabled
if (DISABLED)
return;
update_current_display_mode();
if (aspect_compensation_enabled()) {
// determine the native (maximum) resolution of the primary display; used to
// detect and undo letterboxing when a non-native display mode is in use
detect_native_resolution();
log_info("rawinput", "primary display native size: {}x{}",
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
}
}
}
+494
View File
@@ -0,0 +1,494 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <atomic>
#include <mutex>
#include <windows.h>
#include <commctrl.h>
#include "inject.h"
#include "inject_internal.h"
#include "transform.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr DWORD INJECTION_RETRY_DELAY_MS = 1;
constexpr POINTER_FLAGS CONTACT_DOWN_FLAGS =
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
constexpr POINTER_FLAGS CONTACT_UPDATE_FLAGS =
POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
// Windows 8 APIs are resolved dynamically to preserve older-OS compatibility
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
static decltype(InitializeTouchInjection) *InitializeTouchInjection_ptr = nullptr;
static decltype(InjectTouchInput) *InjectTouchInput_ptr = nullptr;
// state for the single synthetic touch contact
struct ContactState {
ContactOwner owner = ContactOwner::None;
HWND input_window = nullptr;
POINT position {};
UINT_PTR timer_id = 0;
bool is_active() const {
return owner != ContactOwner::None;
}
};
// tracks an injector-owned contact without matching unrelated hardware touches
struct SyntheticTouchIdentity {
POINT down_position {};
HANDLE source = nullptr;
DWORD id = 0;
bool identified = false;
bool pending = false;
bool transform_coordinates = false;
std::atomic<HWND> transform_window { nullptr };
void begin(POINT position, HWND window, bool transform_returned_coordinates) {
down_position = position;
source = nullptr;
id = 0;
identified = false;
pending = true;
transform_coordinates = transform_returned_coordinates;
transform_window.store(window, std::memory_order_release);
}
void reset(HWND expected_window) {
source = nullptr;
id = 0;
identified = false;
pending = false;
transform_coordinates = false;
transform_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
}
HWND get_transform_window() const {
return transform_window.load(std::memory_order_acquire);
}
bool matches(PTOUCHINPUT point) {
// InjectTouchInput provides no application marker in TOUCHINPUT. Claim the first
// matching DOWN, then use the source and ID assigned by Windows for this contact.
if (!identified) {
// correlate the pending injection's pixel position
constexpr LONG POSITION_TOLERANCE = 200;
const auto delta_x = point->x - down_position.x * 100;
const auto delta_y = point->y - down_position.y * 100;
if (!pending || !(point->dwFlags & TOUCHEVENTF_DOWN) ||
delta_x < -POSITION_TOLERANCE || delta_x > POSITION_TOLERANCE ||
delta_y < -POSITION_TOLERANCE || delta_y > POSITION_TOLERANCE) {
return false;
}
// save the identity that remains stable through this contact's MOVE and UP.
source = point->hSource;
id = point->dwID;
identified = true;
}
// require both the provider and contact identities to match.
return point->hSource == source && point->dwID == id;
}
bool should_transform_coordinates() const {
return transform_coordinates;
}
};
static ContactState contact_state;
static SyntheticTouchIdentity synthetic_touch_identity;
// main game window that receives WM_TOUCH forwarded from the dedicated TDJ subscreen
static std::atomic<HWND> touch_delivery_window { nullptr };
// window whose UI thread owns synthetic contact state and synthetic touch requests;
// normally the active touch window, while dedicated TDJ uses the subscreen window
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 int window_subclass_token;
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere
static UINT contact_refresh_message;
// submit one synthetic contact frame to Windows touch injection
static bool inject_touch_frame(
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
POINTER_TOUCH_INFO contact {};
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.pointerFlags = pointer_flags;
contact.pointerInfo.ptPixelLocation = position;
contact.touchFlags = TOUCH_FLAG_NONE;
BOOL result = InjectTouchInput_ptr(1, &contact);
if (result) {
return true;
}
// drop ordinary UPDATE frames when Windows is still processing the prior frame
const auto error = GetLastError();
if (error == ERROR_NOT_READY &&
(pointer_flags & POINTER_FLAG_UPDATE) && !retry_if_not_ready) {
return true;
}
// retry required frames once after a brief delay
if (error == ERROR_NOT_READY) {
Sleep(INJECTION_RETRY_DELAY_MS);
result = InjectTouchInput_ptr(1, &contact);
}
if (result) {
return true;
}
static bool error_logged = false;
if (!error_logged) {
error_logged = true;
log_warning("touch::native", "failed to inject synthetic touch input: {}", GetLastError());
}
return false;
}
// rewrite only the contact created by this injector into game touch coordinates
bool transform_touch_input(PTOUCHINPUT point) {
const auto transform_window = synthetic_touch_identity.get_transform_window();
if (transform_window == nullptr) {
return false;
}
if (!synthetic_touch_identity.matches(point)) {
// this contact is not owned by this injector; leave it unchanged
return false;
}
if (synthetic_touch_identity.should_transform_coordinates()) {
// Windows receives the physical position; the game receives the mapped position
POINT position { point->x / 100, point->y / 100 };
if (transform::screen_to_game(transform_window, &position)) {
point->x = position.x * 100;
point->y = position.y * 100;
}
}
if (point->dwFlags & TOUCHEVENTF_UP) {
synthetic_touch_identity.reset(transform_window);
}
return true;
}
bool contact_is_active() {
return contact_state.is_active();
}
bool contact_is_owned_by(ContactOwner owner, HWND window) {
return contact_state.is_active() &&
contact_state.owner == owner &&
contact_state.input_window == window;
}
bool begin_contact(
ContactOwner owner,
HWND window,
POINT position,
bool transform_returned_coordinates) {
contact_state.owner = owner;
contact_state.input_window = window;
contact_state.position = position;
contact_state.timer_id = 0;
synthetic_touch_identity.begin(position, window, transform_returned_coordinates);
if (inject_touch_frame(position, CONTACT_DOWN_FLAGS)) {
return true;
}
contact_state = {};
synthetic_touch_identity.reset(window);
return false;
}
bool update_contact(ContactOwner owner, HWND window, POINT position) {
if (!contact_is_owned_by(owner, window) ||
!inject_touch_frame(position, CONTACT_UPDATE_FLAGS)) {
return false;
}
contact_state.position = position;
return true;
}
// contact state belongs to the injection window thread; this helper must run there
static void refresh_contact_lifetime_on_window_thread(HWND window) {
if (!contact_is_owned_by(contact_state.owner, window)) {
return;
}
inject_touch_frame(contact_state.position, CONTACT_UPDATE_FLAGS, true);
}
// PAN calls this from ac_io_update to keep a stationary contact alive. Contact state is
// owned by the touch window thread, so same-thread calls refresh immediately. Calls from
// another thread post one coalesced private message for the window thread to handle.
void refresh_contact_lifetime() {
const auto window = injection_window.load(std::memory_order_acquire);
if (window == nullptr) {
return;
}
// preserve synchronous game-loop timing when it already runs on the window thread
const auto window_thread = GetWindowThreadProcessId(window, nullptr);
if (window_thread == GetCurrentThreadId()) {
refresh_contact_lifetime_on_window_thread(window);
return;
}
// otherwise marshal one outstanding refresh instead of sharing contact state across threads
if (contact_refresh_message == 0 ||
contact_refresh_pending.exchange(true, std::memory_order_acq_rel)) {
return;
}
if (!PostMessageW(window, contact_refresh_message, 0, 0)) {
contact_refresh_pending.store(false, std::memory_order_release);
}
}
void set_contact_timer(
ContactOwner owner, HWND window, UINT_PTR timer_id) {
if (contact_is_owned_by(owner, window)) {
contact_state.timer_id = timer_id;
}
}
// end whichever producer currently owns the single synthetic contact
bool release_active_contact() {
if (!contact_state.is_active()) {
return true;
}
const auto input_window = contact_state.input_window;
if (input_window != nullptr && contact_state.timer_id != 0) {
KillTimer(input_window, contact_state.timer_id);
}
const auto result = inject_touch_frame(contact_state.position, POINTER_FLAG_UP);
contact_state = {};
// release mouse capture if this contact owns it
if (input_window != nullptr && GetCapture() == input_window) {
ReleaseCapture();
}
return result;
}
// translate primary mouse messages on a touch window into one touch contact
static LRESULT CALLBACK touch_window_subclass_proc(
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
UINT_PTR subclass_id, DWORD_PTR) {
// consume marshalled lifetime refreshes on the contact-owning window thread
if (contact_refresh_message != 0 && message == contact_refresh_message) {
contact_refresh_pending.store(false, std::memory_order_release);
refresh_contact_lifetime_on_window_thread(window);
return 0;
}
// IIDX handles touch on its main window even when input belongs to the subscreen
// forward the touch message there
if (message == WM_TOUCH &&
transform::is_tdj_dedicated_subscreen(window)) {
const auto delivery_window =
touch_delivery_window.load(std::memory_order_acquire);
if (delivery_window != nullptr) {
return SendMessageW(delivery_window, message, w_param, l_param);
}
}
if (handle_synthetic_message(window, message, w_param, l_param) ||
handle_mouse_message(window, message, w_param)) {
return 0;
}
if (message == WM_NCDESTROY) {
if (contact_state.input_window == window) {
release_active_contact();
}
HWND expected_window = window;
injection_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
contact_refresh_pending.store(false, std::memory_order_release);
touch_delivery_window.store(nullptr, std::memory_order_release);
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
}
return DefSubclassProc(window, message, w_param, l_param);
}
// attach mouse injection without replacing the window's existing procedure
static void attach_window_impl(HWND window, bool register_touch) {
if (!initialize_touch_injection()) {
if (register_touch) {
log_warning(
"touch::native",
"touch injection unavailable; touch window was not registered");
}
return;
}
// if requested, register for touch messages (for cases where the game didn't call it)
if (register_touch &&
(RegisterTouchWindow_orig == nullptr ||
!RegisterTouchWindow_orig(window, TWF_WANTPALM))) {
log_warning(
"touch::native", "failed to register mouse touch window: {}", GetLastError());
return;
}
// set window subclass to intercept messages
if (!SetWindowSubclass(
window,
touch_window_subclass_proc,
reinterpret_cast<UINT_PTR>(&window_subclass_token),
0)) {
log_warning(
"touch::native",
"failed to attach mouse touch injection to window: {}",
GetLastError());
return;
}
// publish the UI-thread target for synthetic touch requests
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
injection_window.store(window, std::memory_order_release);
}
log_misc(
"touch::native", "mouse touch injection attached to window {}", fmt::ptr(window));
}
HWND get_injection_window() {
return injection_window.load(std::memory_order_acquire);
}
void attach_window(HWND window) {
// attach window, but don't register for Windows touch messages
attach_window_impl(window, false);
}
void register_and_attach_window(HWND window) {
// attach window and register for Windows touch messages
attach_window_impl(window, true);
}
// preserve native registration and attach mouse injection to the touch window
static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {
// for TDJ in windowed subscreen mode, the main game window is
// target for delivering touch messages, not the subscreen
if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
touch_delivery_window.store(window, std::memory_order_release);
return TRUE;
}
// call original
const auto result = RegisterTouchWindow_orig(window, flags);
if (result) {
// attach but don't register for touch messages
// (we're already in the middle of RegisterTouchWindow as a result
// of the game calling it)
attach_window(window);
}
return result;
}
static void clear_touch_injection_functions() {
InitializeTouchInjection_ptr = nullptr;
InjectTouchInput_ptr = nullptr;
}
// load and initialize Windows 8 touch injection without a static API dependency
bool initialize_touch_injection() {
std::call_once(initialization_once, [] {
initialize_synthetic_touch();
contact_refresh_message =
RegisterWindowMessageW(L"spice2x.native_touch.refresh_contact");
if (contact_refresh_message == 0) {
log_warning(
"touch::native", "failed to register contact refresh message: {}", GetLastError());
}
// load all APIs from user32 without adding static imports
//
// 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 (InjectTouchInput_ptr == nullptr) {
clear_touch_injection_functions();
log_warning(
"touch::native", "InjectTouchInput unavailable; mouse touch injection disabled");
return;
}
// reserve one synthetic contact and disable Windows' visual touch feedback
if (!InitializeTouchInjection_ptr(1, TOUCH_FEEDBACK_NONE)) {
log_warning(
"touch::native", "failed to initialize mouse touch injection: {}", GetLastError());
clear_touch_injection_functions();
return;
}
log_misc("touch::native", "mouse touch injection initialized");
});
return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
}
// install injection support for touch windows registered by the game module
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_warning("touch::native", "failed to hook RegisterTouchWindow");
return false;
}
log_misc("touch::native", "RegisterTouchWindow hooked");
return true;
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <windows.h>
struct tagTOUCHINPUT;
namespace nativetouch::inject {
void attach_window(HWND window);
void register_and_attach_window(HWND window);
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);
}
@@ -0,0 +1,31 @@
#pragma once
#include <windows.h>
namespace nativetouch::inject {
enum class ContactOwner {
None,
Mouse,
Synthetic,
};
bool initialize_touch_injection();
void initialize_synthetic_touch();
void refresh_contact_lifetime();
bool contact_is_active();
bool contact_is_owned_by(ContactOwner owner, HWND window);
bool begin_contact(
ContactOwner owner,
HWND window,
POINT position,
bool transform_returned_coordinates);
bool update_contact(ContactOwner owner, HWND window, POINT position);
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
bool release_active_contact();
HWND get_injection_window();
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
}
+146
View File
@@ -0,0 +1,146 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <windows.h>
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "touch/touch.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
static int mouse_contact_timer_token;
struct PrimaryMouseButton {
UINT down_message;
UINT double_click_message;
UINT up_message;
WPARAM state_mask;
};
// honor the user's swapped-button setting when choosing the primary button
static PrimaryMouseButton get_primary_mouse_button() {
if (GetSystemMetrics(SM_SWAPBUTTON)) {
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
}
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
}
// use the current physical cursor but reject points outside the subscreen
static bool get_mouse_injection_position(HWND window, POINT *position) {
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
// Windows move its primary pointer back to stale positions during a drag.
if (!GetCursorPos(position)) {
return false;
}
POINT transformed = *position;
return transform::screen_to_game(window, &transformed);
}
// release the active injected contact and its window capture
static void end_mouse_contact(HWND window) {
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
release_active_contact();
}
}
// begin a contact at the physical cursor position and capture future mouse input
static void begin_mouse_contact(HWND window) {
if (contact_is_active()) {
return;
}
POINT position;
if (!get_mouse_injection_position(window, &position) ||
!begin_contact(ContactOwner::Mouse, window, position, true)) {
return;
}
// keep receiving drag messages after the cursor leaves the client area
SetCapture(window);
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
set_contact_timer(ContactOwner::Mouse, window, timer_id);
} else {
log_warning("touch::native", "failed to start mouse touch injection timer");
}
}
}
// update the contact while the primary button remains held
static void move_mouse_contact(
HWND window, WPARAM w_param, WPARAM primary_button_state) {
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
return;
}
POINT position;
if (!get_mouse_injection_position(window, &position)) {
end_mouse_contact(window);
return;
}
if ((w_param & primary_button_state) == 0) {
end_mouse_contact(window);
return;
}
update_contact(ContactOwner::Mouse, window, position);
}
// emit stationary update frames so Windows keeps the contact alive
static void refresh_mouse_contact(HWND window) {
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
return;
}
POINT position {};
if (!GetCursorPos(&position)) {
return;
}
POINT transformed = position;
if (!transform::screen_to_game(window, &transformed)) {
end_mouse_contact(window);
return;
}
update_contact(ContactOwner::Mouse, window, position);
}
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
is_mouse_message_from_touchscreen()) {
return true;
}
if (message == WM_TIMER &&
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
refresh_mouse_contact(window);
return true;
}
const auto primary_button = get_primary_mouse_button();
if (message == primary_button.down_message ||
message == primary_button.double_click_message) {
begin_mouse_contact(window);
} else if (message == WM_MOUSEMOVE) {
move_mouse_contact(window, w_param, primary_button.state_mask);
} else if (message == primary_button.up_message) {
end_mouse_contact(window);
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
message == WM_CAPTURECHANGED) {
end_mouse_contact(window);
}
return false;
}
}
@@ -0,0 +1,175 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <mutex>
#include <windows.h>
#include <windowsx.h>
#include "inject.h"
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "util/logging.h"
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;
void initialize_synthetic_touch() {
std::call_once(synthetic_initialization_once, [] {
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
if (synthetic_touch_message == 0) {
log_warning(
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
}
});
}
// 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) {
// remember when Windows-returned coordinates must map back into game space
const auto transform_returned_coordinates =
transform::is_tdj_dedicated_subscreen(window) ||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
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;
}
if (!begin_contact(
ContactOwner::Synthetic,
window,
position,
transform_returned_coordinates)) {
return;
}
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
} else {
log_warning("touch::native", "failed to start synthetic touch timeout timer");
}
}
static void end_synthetic_contact(HWND window) {
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
release_active_contact();
}
}
bool handle_synthetic_message(
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) };
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;
}
if (message == WM_TIMER &&
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
end_synthetic_contact(window);
return true;
}
return false;
}
static HWND prepare_synthetic_touch() {
if (!initialize_touch_injection()) {
return nullptr;
}
const auto window = get_injection_window();
if (window == nullptr || synthetic_touch_message == 0) {
return nullptr;
}
return window;
}
static bool post_synthetic_touch(
HWND window, POINT position, SyntheticTouchMessage message) {
return PostMessageW(
window,
synthetic_touch_message,
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);
}
}
@@ -0,0 +1,281 @@
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include <atomic>
#include "nativetouchhook.h"
#include "avs/game.h"
#include "rawinput/touch.h"
#include "inject.h"
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "util/detour.h"
#include "util/logging.h"
#define TOUCH_SIMULATE_FAT_FINGERS 0
#define TOUCH_DEBUG_VERBOSE 0
#if TOUCH_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
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 std::atomic<TouchInputFilter> touch_input_filter { 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;
static long native_display_size_y = 1080L;
static void initialize_game_settings() {
const auto is_pan = avs::game::is_model("PAN");
settings::EMULATE_DIGITIZER = is_pan;
// PAN samples and clears touch events in its I/O update loop. Window-timer updates
// made stationary holds flicker while movement updates remained stable, so refresh
// the native contact from ac_io_update instead of using the generic mouse timer.
settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP = is_pan;
// translate native touch between desktop and game-window client coordinates
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES = is_pan;
}
static int WINAPI GetSystemMetricsHook(int index) {
if (index == SM_DIGITIZER) {
return NID_INTEGRATED_TOUCH | NID_EXTERNAL_TOUCH |
NID_MULTI_INPUT | NID_READY;
}
return GetSystemMetrics_orig(index);
}
static void update_native_display_mode() {
RECT display_rect{};
if (GetWindowRect(GetDesktopWindow(), &display_rect)) {
native_display_size_x = display_rect.right - display_rect.left;
native_display_size_y = display_rect.bottom - display_rect.top;
}
DEVMODE display_mode{};
display_mode.dmSize = sizeof(display_mode);
if (EnumDisplaySettingsEx(nullptr, ENUM_CURRENT_SETTINGS, &display_mode, EDS_RAWMODE) &&
(display_mode.dmFields & DM_DISPLAYORIENTATION)) {
native_display_orientation = display_mode.dmDisplayOrientation;
} else {
log_info("touch::native", "failed to determine monitor orientation");
}
log_info(
"touch::native", "primary display mode: {}x{}, orientation {}",
native_display_size_x,
native_display_size_y,
native_display_orientation);
}
static void strip_contact_size(PTOUCHINPUT point) {
#if TOUCH_SIMULATE_FAT_FINGERS
point->dwMask |= 0x004;
point->cxContact = 80 * 100;
point->cyContact = 60 * 100;
#endif
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
// monitors that do set it, IIDX can get very confused (SDVX is not
// affected)
//
// while the test menu and the touch "glow" seem to work properly,
// interacting with subscreen menu items or entering PIN becomes
// very unpredictable
//
// to fix this, simply remove the contact area width and height
//
// note: test menu > I/O > touch test gives 5 numbers:
// n: x, y, w, h
// where
// n is the nth touch input since boot
// x, y are coordinates (center of finger)
// w, h are contact width and height
//
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
// automatically be seen as 1x1, which works perfectly fine
log_debug(
"touch::native",
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
point->x / 100,
point->y / 100,
point->dwMask,
point->cxContact,
point->cyContact);
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
point->cxContact = 0;
point->cyContact = 0;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = native_display_size_x * 100 - point->x;
point->y = native_display_size_y * 100 - point->y;
}
static BOOL WINAPI GetTouchInputInfoHook(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// refresh after exclusive fullscreen establishes the final display mode
if (!native_display_initialized) {
update_native_display_mode();
native_display_initialized = true;
}
// call the original first
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
if (result == 0) {
return result;
}
bool flip_hardware_touch = false;
if (avs::game::is_model("KFC")) {
log_debug(
"touch::native", "orientation = {}, display size = {}x{}",
native_display_orientation,
native_display_size_x,
native_display_size_y);
if (native_display_orientation == DMDO_270) {
flip_hardware_touch = true;
}
}
for (size_t i = 0; i < cInputs; i++) {
PTOUCHINPUT point = &pInputs[i];
const auto synthetic = inject::transform_touch_input(point);
if (avs::game::is_model("LDJ")) {
strip_contact_size(point);
}
const auto flip_values = !synthetic &&
(rawinput::touch::INVERTED ^ flip_hardware_touch);
if (flip_values) {
flip_touch_points(point);
}
if (!synthetic) {
POINT position { point->x / 100, point->y / 100 };
const auto transform_result =
transform::hardware_to_game(&position);
if (transform_result == transform::Result::Transformed) {
point->x = position.x * 100;
point->y = position.y * 100;
} else if (transform_result == transform::Result::Rejected &&
!(point->dwFlags & TOUCHEVENTF_UP)) {
// suppress rejected contacts, but preserve UP to release an active touch ID
point->dwFlags = 0;
}
}
const auto filter = touch_input_filter.load(std::memory_order_acquire);
if (point->dwFlags != 0 && filter != nullptr) {
const NativeTouchEvent event {
.id = point->dwID,
.x = point->x / 100,
.y = point->y / 100,
.down = (point->dwFlags & TOUCHEVENTF_DOWN) != 0,
.move = (point->dwFlags & TOUCHEVENTF_MOVE) != 0,
.up = (point->dwFlags & TOUCHEVENTF_UP) != 0,
.synthetic = synthetic,
};
if (filter(event)) {
point->dwFlags = 0;
}
}
}
return result;
}
bool is_hooked() {
return native_touch_hooked;
}
void set_input_filter(TouchInputFilter filter) {
touch_input_filter.store(filter, std::memory_order_release);
}
void refresh_contact_lifetime() {
if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
inject::refresh_contact_lifetime();
}
}
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();
// 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;
}
// 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_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_warning("touch::native", "failed to hook GetTouchInputInfo");
return false;
}
log_misc("touch::native", "GetTouchInputInfo hooked");
native_touch_hooked = true;
return true;
}
}
@@ -0,0 +1,26 @@
#pragma once
#include <windows.h>
namespace nativetouch {
struct NativeTouchEvent {
DWORD id;
LONG x;
LONG y;
bool down;
bool move;
bool up;
bool synthetic;
};
using TouchInputFilter = bool (*)(const NativeTouchEvent &event);
bool hook(HMODULE module);
void refresh_contact_lifetime();
void set_input_filter(TouchInputFilter filter);
// 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();
}
+7
View File
@@ -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;
}
+141
View File
@@ -0,0 +1,141 @@
#include "transform.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "settings.h"
#include "touch/touch.h"
namespace nativetouch::transform {
static bool game_client_to_screen(HWND window, POINT *position) {
RECT client_rect {};
if (window == nullptr ||
!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
!PtInRect(&client_rect, *position)) {
return false;
}
return ClientToScreen(window, position) != FALSE;
}
static bool screen_to_game_client(HWND window, POINT *position) {
RECT client_rect {};
if (window == nullptr ||
!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
!ScreenToClient(window, position) ||
!PtInRect(&client_rect, *position)) {
return false;
}
return true;
}
bool is_tdj_dedicated_subscreen(HWND window) {
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
window == TDJ_SUBSCREEN_WINDOW;
}
// convert game touch coordinates to Windows desktop coordinates
bool game_to_screen(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
return game_client_to_screen(window, position);
}
if (!is_tdj_dedicated_subscreen(window)) {
return true;
}
RECT client_rect {};
if (!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
return false;
}
position->x = MulDiv(
position->x - SPICETOUCH_TOUCH_X,
client_rect.right,
SPICETOUCH_TOUCH_WIDTH);
position->y = MulDiv(
position->y - SPICETOUCH_TOUCH_Y,
client_rect.bottom,
SPICETOUCH_TOUCH_HEIGHT);
return ClientToScreen(window, position) != FALSE;
}
static bool has_active_overlay_transform() {
return overlay::OVERLAY != nullptr &&
overlay::OVERLAY->get_active() &&
overlay::OVERLAY->can_transform_touch_input();
}
static bool transform_overlay_touch_position(POINT *position) {
// convert physical screen coordinates to the window-relative coordinates the overlay expects
if (GRAPHICS_WINDOWED) {
position->x -= SPICETOUCH_TOUCH_X;
position->y -= SPICETOUCH_TOUCH_Y;
}
// ask the overlay to do the game-specific translation
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
}
// convert physical screen coordinates to game touch coordinates for a known target
bool screen_to_game(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
return screen_to_game_client(window, position);
}
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
if (is_tdj_dedicated_subscreen(window)) {
RECT client_rect {};
if (!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
return false;
}
if (!ScreenToClient(window, position)) {
return false;
}
if (!PtInRect(&client_rect, *position)) {
return false;
}
position->x = SPICETOUCH_TOUCH_X +
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
position->y = SPICETOUCH_TOUCH_Y +
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
return true;
}
// check if subscreen overlay is active and can transform the touch point;
// if not, the touch point is valid as-is
if (!has_active_overlay_transform()) {
return true;
}
// ask the overlay to transform the touch point into game coordinates
return transform_overlay_touch_position(position);
}
// route hardware screen coordinates through dedicated or overlay mapping and report the result
Result hardware_to_game(POINT *position) {
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
const auto active_overlay = has_active_overlay_transform();
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
if (!dedicated_subscreen && !active_overlay) {
return Result::Unchanged;
}
// route through the dedicated subscreen when active, otherwise through the overlay
const auto valid = screen_to_game(
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
position);
// reject out-of-bounds points and any coordinate conversion failure
return valid ? Result::Transformed : Result::Rejected;
}
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <windows.h>
namespace nativetouch::transform {
enum class Result {
Unchanged,
Transformed,
Rejected,
};
bool is_tdj_dedicated_subscreen(HWND window);
bool game_to_screen(HWND window, POINT *position);
bool screen_to_game(HWND window, POINT *position);
Result hardware_to_game(POINT *position);
}
+1 -3
View File
@@ -81,8 +81,6 @@ static const char *LOG_MODULE_NAME = "touch";
static TouchHandler *TOUCH_HANDLER = nullptr;
static bool is_mouse_message_from_touchscreen();
TouchHandler::TouchHandler(std::string name) {
log_info("touch", "Using touch handler: {}", name);
}
@@ -1003,7 +1001,7 @@ void update_spicetouch_window_dimensions(HWND hWnd) {
}
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
static bool is_mouse_message_from_touchscreen() {
bool is_mouse_message_from_touchscreen() {
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
+1
View File
@@ -29,6 +29,7 @@ extern int SPICETOUCH_TOUCH_WIDTH;
extern int SPICETOUCH_TOUCH_HEIGHT;
bool is_touch_available(LPCSTR caller);
bool is_mouse_message_from_touchscreen();
void touch_attach_wnd(HWND hWnd);
void touch_attach_dx_hook();