Compare commits

...

51 Commits

Author SHA1 Message Date
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
bicarus-dev 2aae8ad9ca move updater scripts 2026-07-15 02:14:19 -07:00
bicarus 1b6d1af951 jb, shogikai: double buffer overlay, run at ~60fps (#805)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
GDI bit blit overlay used exclusively for jubeat & shogikai was running
at 30fps, which caused the Jubeat touch debug UI to not render fast
enough. Run it at 60 fps, and also perform double buffering to avoid
flickering.

This could have been scoped to JB but I made the change for both. XP
builds will keep running at 30fps since those people run on cabs with
ancient hardware.

## Testing
Tested jubeat and shogikai, overlay renders without flickering at 60 fps
2026-07-15 01:31:29 -07:00
bicarus 5c3617c05f rawinput: fix ttl logic in raw touch (#806)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fixes held touch contacts briefly dropping under rapid multi-touch (e.g.
holding several jubeat buttons and tapping elsewhere many times would
make the holds briefly release and the hold again).

A HID digitizer reports a fixed number of contact slots per report and
cycles the rest through later reports, so the touch layer keeps an
unreported contact alive for a few reports (a per-contact TTL). The TTL
was only set for contacts in the current report and had no slack, so
when the contact count rose (from the rapid taps) the reporting cycle
got longer and stationary holds were expired before the device cycled
back to them.

Now, the cycle length is recomputed every report and every tracked
contact's TTL is extended to it (never shrunk), so a rising contact
count can't starve a currently held contact. The 50 ms last-report
timeout still removes genuinely lifted contacts.

## Testing
Tested on jb, rb.
2026-07-14 23:46:17 -07:00
bicarus a9a943fbfe jb: fix card not detected on title screen (#804)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long standing issue with card reader behavior in jubeat.

sciunit emulation has a bug - when on the title screen / demo loop,
inserting a card did not generate an interrupt. Users always had to
press the game start button and card in. Fix this by reversing what the
actual sciunit does and mirroring the state machine behavior.

## Testing
Tested L44 and K44.
2026-07-14 17:06:00 -07:00
bicarus-dev b094a9e352 deprecate insert card button 2026-07-14 02:49:46 -07:00
bicarus 4bd18afb12 rb: fix alignment of vertical lines in debug view (#803)
they were off by third of a column
2026-07-14 00:18:55 -07:00
bicarus 0b24e703a1 overlay: re-enable keyboard navigation (#802)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #800 

## Description of change
Restores keyboard navigation by default in spicecfg, and add an option
to optionally enable it in the overlay.

## Testing
2026-07-14 00:18:21 -07:00
bicarus 1a1be7a393 rb: touch debug overlay, tweak scaling (#801)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to #797, draw an overlay to debug touch issues for reflec beat.

For rb, we draw on the ImGui surface. Draw horizontal and vertical line
for triggered sensors, and also a bounding box if scaling is in use.

Make small tweaks to how scaling works.

## Testing
Verified volzza 2 / reflesia / 1st game (landscape KBR)
2026-07-13 23:30:41 -07:00
bicarus a2d8f60d12 jb: only show debug overlay for jubeat (#799) 2026-07-13 19:42:27 -07:00
bicarus 5d25fdb035 jb: touch debounce (#798)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Debouce option for touch input. Applies to all touch handlers (rawinput,
win8, win7) since it sits at the touch layer, and then consumed by jb
touch logic.

## Testing
2026-07-13 02:06:54 -07:00
bicarus c255e3a1db jb: option to draw debug graphics to show touch interactions (#797)
## Link to GitHub Issue or related Pull Request, if one exists
related to #697 

## Description of change
Add a new option that paints touch targets and touch points for jubeat;
gets enabled if there is a touch screen connected. This is to help
explain / debug issues when users have their touch screen issues, which
is usually due to misalignment.

Fix the touch targets being off by 1 pixel (center gap is 1px wider in
both directions).

Also, simplify the `improved` algorithm to simulate a finger and trigger
nearest-neighbor for the AC dimensions. In terms of touch performance
this is near identical to what we had before, but it allows us to draw a
nice debug overlay instead of just grids.

## Testing
Tested portrait and landscape versions of jb.
2026-07-13 00:31:05 -07:00
bicarus d7c144646f rawinput: deal with high poll rate devices, use 3x3 for default for rb touch emu (#796)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Two changes:

1. Handle all HID events in `WM_INPUT` and not just the first one. This
only really affects 4000Hz / 8000Hz HID devices.
2. Go back to `3x3` as the default for rb IR touch emulation and remove
`1x1` as an option as it does not perform as well as I expected.

## Testing
2026-07-12 10:32:29 -07:00
bicarus-dev 27077aa21f bump action-gh-release version 2026-07-12 01:45:16 -07:00
bicarus-dev 632dcf7b41 clean up option text for consistency 2026-07-12 00:00:31 -07:00
bicarus a25bbdc9c9 rawinput: refactor midi (#795)
pure movement of code only, no functional changes
2026-07-11 23:59:34 -07:00
bicarus 47d886306e rawinput: handle midi scanning asynchronously, fix midi hotplug (#793)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #603

## Description of change
Scanning for midi devices can take a while on Windows 11 with MIDI 2.0
service. Sometimes it'll take a couple seconds. Sometimes, on a PC with
zero MIDI devices, it takes 10-11 seconds.

This was causing two issues:

1. slow startup time
2. crash on invalid memory access due to a race condition

Address both.

Also fix a bug: when a device change event fires, we do a MIDI scan, and
invalidated all existing MIDI devices in favor of creating new handles.
Stop doing this, and instead check for duplicates by matching the ID and
keep existing device handles alone. Properly clean up devices on unplug.

Also: fix MIDI buttons being stuck on when unplugged while holding a
key.

## Testing
Tested with Nostroller in MIDI mode. rtpMIDI works too.
2026-07-11 21:57:55 -07:00
bicarus-dev edda658fa4 update rawtouchaspect text 2026-07-11 21:16:34 -07:00
bicarus-dev 1ab20f1b56 update wintouch option text 2026-07-11 21:06:29 -07:00
bicarus 0e9093a74b xinput: allow for newer xinput DLL (#794)
Fixes #792
2026-07-11 03:04:49 -07:00
bicarus f52eaea76c touch: fix indicators showing in touch overlay, disable additional gestures (#791) 2026-07-11 00:24:36 -07:00
bicarus c3fcf2f417 touch: rawinput touch to compensate for letterboxing (#790)
## Link to GitHub Issue or related Pull Request, if one exists

Fixes #697

## Description of change

Adds aspect-ratio compensation for raw input touch handler.

This addresses the problem with playing rawinput touch games - namely
jubeat and reflec beat - on a display that is not 16:9, and user chose
to preserve the aspect ratio. This happens a lot on:

* 16:10 touch screens
* touch screen laptops (which range from 3:2, 16:10, and so on).

Root cause is a **Windows Raw Input limitation**. Unlike
`WM_POINTER`/`WM_TOUCH`, where the OS already maps digitizer coordinates
to screen space (applying the display's calibration/scaling), Raw Input
delivers the HID digitizer's *raw* logical coordinates, spanning the
whole physical panel, with **no calibration or display-mapping data
attached**. We have to reconstruct that mapping ourselves.

When a game runs at a display mode whose aspect ratio differs from the
panel's native resolution, Windows scales the image with
letterboxing/pillarboxing (black bars). That is standard OS/GPU
behavior. Because Raw Input gives us no mapping to account for those
bars, touches land off-target. This change detects the mismatch and
remaps each touch from panel-space into the displayed image region;
touches inside the black bars are treated as released.

Adds new launcher option **`-rawtouchaspect`**.

Notes:

- Native resolution is the largest-area mode from `EnumDisplaySettings`
on the **primary** display, consistent with the existing primary-only
touch pipeline.
- Math assumes centered, aspect-preserving scaling; only one axis is
ever inset.
- A small epsilon (0.01) makes near-matched panels (e.g. 1366×768 vs
true 16:9) a no-op.
- If the user forces a stretched image, then the touches will be off
again. In this case, the user needs to use the `force off` setting.

## Testing

On my tiny Surface Go tablet, 
* checked that jubeat squares align correctly
* good enough to full combo level 11 for reflec beat
2026-07-10 23:23:45 -07:00
bicarus 8d04eba21d build: check for win7 compat, fix 32-bit xp check (#789)
## Link to GitHub Issue or related Pull Request, if one exists
#787 

## Description of change
Updates usage of `windows_dll_compat_checker`

## Testing
it builds
2026-07-10 01:51:04 -07:00
bicarus 8d42c0ce77 asio: fix XP build; remove call to RegGetValueA (#788)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #787 

## Description of change
Remove the call to `RegGetValueA` as it is Vista and up. Replace with
`RegOpenKeyExA` + `RegQueryValueExA`

## Testing
2026-07-10 00:26:03 -07:00
bicarus-dev 829503568e fix UI text for -sdvxnolive2d 2026-07-06 01:27:18 -07:00
bicarus 746fdb5409 build: manual update scripts (#786)
people asked for this often enough
2026-07-02 23:48:52 -07:00
bicarus 67da892fe9 cfg: aliases for -network and -subnet (#785)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
`-network` and `-subnet` are poorly named. Add new names for them that
are more explicit.

Old command line parameters will continue to work, this is an additive
change.

## Testing
2026-07-01 02:41:14 -07:00
bicarus dc191487e0 Update CONTRIBUTING.md 2026-06-30 17:45:23 -07:00
bicarus 5c0c405800 Update CONTRIBUTING.md 2026-06-30 14:32:30 -07:00
bicarus-dev 60166e6ba1 update gh actions 2026-06-30 02:29:44 -07:00
bicarus cb0b59600a build: GitHub action for creating a draft relase (#783) 2026-06-30 00:53:08 -07:00
bicarus b9f3be4df2 asio: cache result from CoCreateInstance to work around buggy asio drivers (#781)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
IIDX likes to call `CoCreateInstance` and `init` multiple times on ASIO
drivers, but some ASIO drivers like `Neva Uno` really don't like that
and ends up crashing. In our wrapper, cache the instances and try to
reuse them for better compatibility.

## Testing
Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these
drivers repro the crashing behavior.
2026-06-28 22:37:48 -07:00
121 changed files with 10684 additions and 5133 deletions
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA
id: vars
run: |
@@ -25,7 +25,7 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
+49
View File
@@ -0,0 +1,49 @@
name: Draft Release
on:
workflow_dispatch:
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build and Draft Release
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
- name: Clean leftover build artifacts
run: |
rm -rf .ccache dist bin cmake-build-*
- name: Compile
run: ./build_docker.sh
- name: Determine release name from dist filename
run: |
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
name="${dist%.zip}"
# the tag is the date portion, e.g. 26-06-28
tag="${name#spice2x-}"
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
- name: Create draft release
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: true
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_NAME }}
target_commitish: main
generate_release_notes: true
files: |
src/spice2x/dist/spice2x-*.zip
+7 -3
View File
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
### Forbidden "features" that will never be accepted
### Pull requests that will not be accepted
* Performing decryption of encrypted files
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
* Performing decryption of encrypted files.
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
* This includes automatic updaters.
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
* Built-in EA server that saves data. They should live in a separate project.
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
* Changes to the software license terms.
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
### Avoiding regressions
+22 -2
View File
@@ -412,10 +412,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/sdvx/io.cpp
games/sdvx/camera.cpp
games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp
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
@@ -431,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/rb/rb.cpp
games/rb/io.cpp
games/rb/touch.cpp
games/rb/touch_debug.cpp
games/bs/bs.cpp
games/bs/io.cpp
games/rf3d/rf3d.cpp
@@ -500,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
@@ -526,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
@@ -569,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
@@ -607,17 +611,27 @@ 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
rawinput/touch.cpp
@@ -644,8 +658,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
stubs/stubs.cpp
# 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_indicators.cpp
touch/touch_gestures.cpp
touch/win7.cpp
touch/win8.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);
};
+18 -1
View File
@@ -214,6 +214,19 @@ time (
echo "==========================="
)
echo ""
echo "Checking Win7 compatibility..."
echo "============================="
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
else
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
${BUILDDIR_64}/spicetools/64/spice64.exe
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
${BUILDDIR_32}/spicetools/spicecfg.exe \
${BUILDDIR_32}/spicetools/32/spice.exe
fi
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
echo ""
@@ -229,7 +242,7 @@ then
fi
if ((BUILD_XP_32 > 0))
then
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
# generate the standalone updater scripts from the shared template and drop them
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
# build distribution archive
if ((DIST_ENABLE > 0))
then
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# builds the standalone updater scripts from the shared PowerShell logic in
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
# header (with the release channel baked in) followed by the shared PowerShell
# body after the marker line.
#
# usage: build_updaters.sh OUT_DIR
# OUT_DIR directory to write the generated .bat files into
set -eu
if [ $# -lt 1 ]; then
echo "usage: build_updaters.sh OUT_DIR" >&2
exit 1
fi
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PS_BODY="${SRC_DIR}/update_spice.ps1"
OUT_DIR="$1"
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
MARKER='#___PS___'
gen() {
channel="$1"; label="$2"; out="$3"
{
cat <<HEADER
@echo off
setlocal
REM ============================================================================
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
REM this folder to the latest spice2x release from GitHub.
REM
REM release channel: ${label}
REM make sure spice/spicecfg are NOT running before updating.
REM ============================================================================
REM expose the script folder + channel to the embedded PowerShell, then run the
REM PowerShell part: re-read this file and execute everything after the marker.
set "SPICE_DIR=%~dp0"
set "SPICE_CHANNEL=${channel}"
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
${MARKER}
HEADER
cat "$PS_BODY"
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
echo " generated ${OUT_DIR}/${out}"
}
echo "Generating updater scripts from $(basename "$PS_BODY")..."
gen "" "stable (pre-releases excluded)" "update_spice.bat"
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
+72 -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;
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break;
}
case rawinput::DESTROYED:
// device was unplugged. release the button instead of leaving
// state at getLastState(), which would latch a held button on
// forever (e.g. a MIDI note held while the device is removed)
state = BUTTON_NOT_PRESSED;
break;
default:
break;
}
@@ -463,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);
@@ -478,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");
-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"
+2 -4
View File
@@ -11,8 +11,8 @@
#include "hooks/audio/audio.h"
#include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h"
@@ -653,9 +653,7 @@ 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);
nativetouch::hook(avs::game::DLL_INSTANCE);
#if !SPICE_XP
+2 -10
View File
@@ -20,8 +20,7 @@
#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 "util/detour.h"
#include "util/deferlog.h"
@@ -63,7 +62,6 @@ namespace games::iidx {
bool TDJ_MODE = false;
bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = 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,13 +348,7 @@ 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 {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
}
nativetouch::hook(avs::game::DLL_INSTANCE);
}
// insert BI2X hooks
-1
View File
@@ -28,7 +28,6 @@ namespace games::iidx {
extern bool TDJ_MODE;
extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS;
+19 -144
View File
@@ -3,30 +3,15 @@
#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,80 +101,13 @@ 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());
}
}
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();
const auto &touch = touch_points.front();
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
}
void enable() {
@@ -210,35 +128,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 +159,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 +188,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);
+5 -210
View File
@@ -3,212 +3,24 @@
#include <windows.h>
#include <filesystem>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "hooks/graphics/graphics.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/detour.h"
#include "util/libutils.h"
#define JB_BUTTON_SIZE 160
#define JB_BUTTON_GAP 37
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
namespace games::jb {
// touch stuff
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static std::vector<TouchPoint> TOUCH_POINTS;
bool TOUCH_STATE[16];
void touch_update() {
// check if touch enabled
if (!TOUCH_ENABLE) {
return;
}
// attach touch module
if (!TOUCH_ATTACHED) {
/*
* Find the game window.
* We check the foreground window first, then fall back to searching for the window title
* All game versions seem to have their model first in the window title
*/
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
}
// check if we have a window handle
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE = false;
return;
}
// attach touch hook
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
touch_create_wnd(wnd, true);
// show cursor
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
// earlier games use a different screen orientation
if (!avs::game::is_model("L44")) {
IS_PORTRAIT = false;
}
// set attached
TOUCH_ATTACHED = true;
}
// reset touch state
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
// check touch points
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS);
if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) {
// get grid coordinates
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
// set the corresponding state
int index = y * 4 + x;
if (index >= 0 && index < 16) {
TOUCH_STATE[index] = true;
}
}
} else {
for (auto &tp : TOUCH_POINTS) {
// check window out of bounds
if (IS_PORTRAIT) {
if (tp.x > 768 || tp.y > 1360) {
continue;
}
} else {
if (tp.x > 1360 || tp.y > 768) {
continue;
}
}
int x_relative = tp.x;
int y_relative = tp.y;
// x_relative and y_relative are relative to the top-left pixel of the first button
if (IS_PORTRAIT) {
// which is at (8, 602) in portrait:
// X: [8...759] (752 pixels wide)
// Y: [602...1353] (752 pixels high)
x_relative -= 8;
y_relative -= 602;
} else {
// and at (8, 8) in landscape
x_relative -= 8;
y_relative -= 8;
}
int x_index = -1;
int x_hitbox = 0;
int y_index = -1;
int y_hitbox = 0;
// x_hitbox and y_hitbox is relative to top-left pixel of each button
if (x_relative >= 0) {
x_index = x_relative / JB_BUTTON_HITBOX;
x_hitbox = x_relative % JB_BUTTON_HITBOX;
}
if (y_relative >= 0) {
y_index = y_relative / JB_BUTTON_HITBOX;
y_hitbox = y_relative % JB_BUTTON_HITBOX;
}
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
if (TOUCH_ALGORITHM == Improved) {
if (IS_PORTRAIT) {
// extend to left border
if (x_relative < 0) {
x_index = 0;
}
// right and bottom borders are covered by the hit box
} else {
// extend to top border
if (y_relative < 0) {
y_index = 0;
}
// extend to left border
if (x_relative < 0) {
x_index = 0;
}
// bottom border is covered by the hit box
// rightmost edge - ignore them entirely
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
continue;
}
}
}
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
continue;
}
// check if the gap was touched
if (TOUCH_ALGORITHM == AcAccurate) {
// in ac-accurate mode, touching the gap is ignored
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
continue;
}
} else if (TOUCH_ALGORITHM == Improved) {
// in improved mode, touching the gap triggers the closest button
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
x_index++;
}
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
y_index++;
}
}
// set the corresponding state
int index = y_index * 4 + x_index;
if (0 <= index && index < 16) {
TOUCH_STATE[index] = true;
}
}
}
}
/*
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
*/
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() {
return 0;
}
/*
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
*/
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
static BOOL __stdcall network_get_network_check_info() {
return 0;
}
/*
* to fix network error on non DHCP interfaces
*/
// fixes network errors on non-DHCP interfaces
static BOOL __cdecl network_get_dhcp_result() {
return 1;
}
@@ -248,23 +60,7 @@ namespace games::jb {
void JBGame::attach() {
Game::attach();
// enable touch
TOUCH_ENABLE = true;
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
break;
}
touch_attach();
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
@@ -284,7 +80,6 @@ namespace games::jb {
void JBGame::detach() {
Game::detach();
// disable touch
TOUCH_ENABLE = false;
touch_detach();
}
}
+1 -11
View File
@@ -1,20 +1,10 @@
#pragma once
#include "games/game.h"
#include "games/jb/jb_touch.h"
namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16];
void touch_update();
class JBGame : public games::Game {
public:
JBGame();
+471
View File
@@ -0,0 +1,471 @@
#include "jb_touch.h"
#include <windows.h>
#include <atomic>
#include <cmath>
#include <limits>
#include <vector>
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "rawinput/touch.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
// (752px across). the first button's top-left is at (8, 602) in portrait and
// (6, 8) in landscape.
#define JB_BUTTON_SIZE 160
#define JB_MAX_BUTTON_GAP 38
// improved and plus modes use this reach around each button. must be >= the
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
#define JB_TOUCH_RADIUS 38
namespace games::jb {
static_assert(std::atomic_bool::is_always_lock_free);
static_assert(std::atomic_uint16_t::is_always_lock_free);
static_assert(std::atomic<POINT>::is_always_lock_free);
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
// touch state
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
uint32_t TOUCH_DEBOUNCE_MS = 0;
static std::atomic_bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static std::atomic_uint16_t TOUCH_STATE = 0;
// fixed-size contact view used by the debug overlay
static const size_t JB_MAX_TOUCH_POINTS = 16;
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
static constexpr POINT JB_INVALID_TOUCH_POINT {
JB_INVALID_TOUCH_COORD,
JB_INVALID_TOUCH_COORD
};
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
static void clear_touch_points() {
for (auto &point : TOUCH_POINTS) {
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
}
}
// false when a contact is younger than the debounce window and should be ignored
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
return now_ms - tp.down_ms >= threshold_ms;
}
// snapshot for the debug overlay: each slot atomically holds one matured contact or
// the invalid sentinel. immature and absent contacts are published as invalid, so the
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
double now_ms, double threshold_ms) {
size_t count = touch_points.size();
if (count > JB_MAX_TOUCH_POINTS) {
count = JB_MAX_TOUCH_POINTS;
}
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
POINT point = JB_INVALID_TOUCH_POINT;
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
point = { touch_points[i].x, touch_points[i].y };
}
TOUCH_POINTS[i].store(point, std::memory_order_release);
}
}
// --- touch geometry ------------------------------------------------------
// gaps between the four buttons along one axis (the middle gap is 1px wider)
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
struct AxisGeometry {
int button[4]; // left/top edge of each button
};
// left/top edges of the four buttons along one axis, starting at `first`
static AxisGeometry axis_geometry(int first) {
AxisGeometry g {};
g.button[0] = first;
for (int i = 1; i < 4; i++) {
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
}
return g;
}
// button edges for the current orientation
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
if (IS_PORTRAIT) {
gx = axis_geometry(8);
gy = axis_geometry(602);
} else {
gx = axis_geometry(6);
gy = axis_geometry(8);
}
}
// distance from `p` to a button along one axis (0 when inside)
static int axis_distance(int p, int button) {
int end = button + JB_BUTTON_SIZE - 1;
if (p < button) {
return button - p;
}
if (p > end) {
return p - end;
}
return 0;
}
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
// shared by detection and the debug overlay so both agree which button a touch hits
static int nearest_button(
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
int best_index = -1;
int best_dist = 0;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
int dist = dx * dx + dy * dy;
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
best_dist = dist;
best_index = r * 4 + c;
}
}
}
return best_index;
}
// detection reach for the current algorithm (0 = register only inside a button)
static int touch_radius() {
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
}
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
// every button within `radius` when `multi` (edge/gap presses trigger several)
static void mark_buttons(uint16_t &state, int px, int py,
const AxisGeometry &gx, const AxisGeometry &gy,
int radius, bool multi) {
if (!multi) {
int index = nearest_button(px, py, gx, gy, radius);
if (index >= 0) {
state |= uint16_t(1) << index;
}
return;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
if (dx * dx + dy * dy <= radius * radius) {
state |= uint16_t(1) << (r * 4 + c);
}
}
}
}
std::bitset<16> touch_state() {
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
}
void touch_update() {
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
// one-time touch window attach
if (!TOUCH_ATTACHED) {
// find the game window: prefer the foreground window, else search by
// title (the model name prefixes the window title in every version)
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
}
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
return;
}
// only the L44 model runs in portrait; set this before starting the
// touch-window thread so the renderer only observes the final value
IS_PORTRAIT = avs::game::is_model("L44");
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
// 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);
}
TOUCH_ATTACHED = true;
}
// calculate the next state locally and publish it after processing every touch
uint16_t next_state = 0;
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
// debounce: the landing time is stamped by the touch layer, so this is
// independent of how often the game polls for input (0 = off)
double now_ms = get_performance_milliseconds();
double threshold_ms = TOUCH_DEBOUNCE_MS;
// publish every raw contact (with its maturity) for the debug overlay, then drop
// the ones still inside the debounce window so no algorithm sees them
publish_touch_points(touch_points, now_ms, threshold_ms);
std::erase_if(touch_points, [&](const TouchPoint &tp) {
return !touch_matured(tp, now_ms, threshold_ms);
});
if (TOUCH_ALGORITHM == Legacy) {
// legacy: evenly divide the play area into a 4x4 grid
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : touch_points) {
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
int index = y * 4 + x;
if (index >= 0 && index < 16) {
next_state |= uint16_t(1) << index;
}
}
} else {
// accurate registers only a touch inside a button; improved snaps each touch
// to the single nearest button within reach (so a gap or centre touch still
// triggers exactly one button); plus marks every button within the same reach,
// so an edge or gap touch can trigger several at once (like the mobile game)
AxisGeometry gx, gy;
touch_geometry(gx, gy);
int radius = touch_radius();
bool multi = (TOUCH_ALGORITHM == Plus);
for (auto &tp : touch_points) {
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
}
}
TOUCH_STATE.store(next_state, std::memory_order_release);
}
// true when any supported touch handler detects a touchscreen (checked once)
static bool touchscreen_detected() {
static const bool detected = is_touch_available("jubeat touch debug overlay");
return detected;
}
// auto draws the boxes only when a touch screen is present
static bool debug_show_boxes() {
switch (TOUCH_DEBUG_OVERLAY) {
case JbTouchDebugBox:
case JbTouchDebugAll:
return true;
case JbTouchDebugAuto:
return touchscreen_detected();
default:
return false;
}
}
static bool debug_show_taps() {
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
}
bool touch_debug_overlay_enabled() {
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
(debug_show_boxes() || debug_show_taps());
}
// the 16 boundary rects: legacy divides the area evenly, others use button squares
static void debug_cells(
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
if (legacy) {
int offset = IS_PORTRAIT ? 580 : 0;
int x_edges[5];
int y_edges[5];
for (int i = 0; i <= 4; i++) {
x_edges[i] = i * 768 / 4;
y_edges[i] = offset + i * (1360 - 580) / 4;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
}
}
} else {
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = {
gx.button[c], gy.button[r],
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
};
}
}
}
}
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
static void draw_debug_boxes(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
RECT cells[16];
debug_cells(legacy, gx, gy, cells);
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
auto state = touch_state();
for (int i = 0; i < 16; i++) {
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
SelectObject(hdc, old_pen);
}
DeleteObject(pen_idle);
DeleteObject(pen_active);
}
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
// reaches across gaps to nearby buttons, and the others only fire inside a button square
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
bool legacy, int radius) {
if (legacy) {
return true;
}
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
return nearest_button(px, py, gx, gy, reach) >= 0;
}
// 90-degree arc on the circle rim facing the centre of button `index`
static void draw_tap_arc(HDC hdc, int px, int py, int index,
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
int c = index % 4;
int r = index / 4;
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
const double quarter = 3.14159265358979323846 / 2.0;
const int segments = 16;
POINT arc[segments + 1];
for (int i = 0; i <= segments; i++) {
double a = mid - quarter / 2.0 + quarter * i / segments;
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
}
Polyline(hdc, arc, segments + 1);
}
// circle at each touch: white when it presses a button, grey otherwise; in single-button
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
// plus, where a touch can trigger several buttons at once)
static void draw_debug_taps(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
// PS_INSIDEFRAME keeps the stroke inside the circle radius
const int stroke = 4;
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
// accurate has no reach, so fall back to a visible marker size when drawing the circle
int radius = touch_radius();
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
// each slot holds a matured contact or the invalid sentinel; immature and absent
// contacts were already filtered out by publish_touch_points
for (auto &touch_point : TOUCH_POINTS) {
POINT point = touch_point.load(std::memory_order_acquire);
if (point.x == JB_INVALID_TOUCH_COORD) {
continue;
}
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
SelectObject(hdc, pressed ? pen_white : pen_gray);
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
point.x + draw_radius, point.y + draw_radius);
// the arc points at a single snapped-to button; plus can trigger several at
// once, so skip it there
if (pressed || TOUCH_ALGORITHM == Plus) {
continue;
}
int index = nearest_button(point.x, point.y, gx, gy, radius);
if (index >= 0) {
SelectObject(hdc, pen_white);
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
}
}
SelectObject(hdc, old_pen);
DeleteObject(pen_white);
DeleteObject(pen_gray);
}
void touch_draw_debug_overlay(HDC hdc) {
// only draw while touch is active
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
bool show_boxes = debug_show_boxes();
bool show_taps = debug_show_taps();
if (!show_boxes && !show_taps) {
return;
}
// legacy divides the field evenly; the other algorithms use button squares
bool legacy = (TOUCH_ALGORITHM == Legacy);
AxisGeometry gx {}, gy {};
if (!legacy) {
touch_geometry(gx, gy);
}
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
if (show_boxes) {
draw_debug_boxes(hdc, legacy, gx, gy);
}
if (show_taps) {
draw_debug_taps(hdc, legacy, gx, gy);
}
SelectObject(hdc, old_brush);
}
void touch_attach() {
clear_touch_points();
TOUCH_ENABLE.store(true, std::memory_order_release);
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case Plus:
log_info("jubeat", "using 'plus' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo, this is a bug");
break;
}
}
void touch_detach() {
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
}
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <bitset>
#include <cstdint>
#include <windows.h>
namespace games::jb {
// touch detection algorithm
enum JubeatTouchAlgorithm {
Legacy,
Improved,
Plus,
AcAccurate
};
// debug touch overlay mode
enum JubeatTouchDebugMode {
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
JbTouchDebugNone, // draw nothing
JbTouchDebugBox, // boundary boxes
JbTouchDebugAll, // boundary boxes and touch circles
};
// selected algorithm and debug mode (set from the launcher options)
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
// minimum contact age in milliseconds before a touch registers (0 = off);
// filters momentary phantom touches from noisy panels
extern uint32_t TOUCH_DEBOUNCE_MS;
// atomically published panel state, read by the I/O layer
std::bitset<16> touch_state();
// read the current touch points and refresh the panel state
void touch_update();
// enable/disable touch handling on game attach/detach
void touch_attach();
void touch_detach();
// whether the debug overlay should currently draw anything
bool touch_debug_overlay_enabled();
// draw the debug touch overlay (boundary boxes and/or touch circles per the
// selected mode) onto the spice touch overlay window's device context
void touch_draw_debug_overlay(HDC hdc);
}
-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"
+10 -8
View File
@@ -1,16 +1,14 @@
// 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 +41,10 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings);
// custom touch
// nostalgia crashes if you inject touch events too early
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
nativetouch::hook(avs::game::DLL_INSTANCE);
if (ENABLE_TOUCH_MODE) {
touch_mode::enable();
}
}
void NostGame::post_attach() {
@@ -55,6 +54,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:
+87 -75
View File
@@ -1,22 +1,24 @@
#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 "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke {
static std::thread *THREAD = nullptr;
static volatile bool THREAD_RUNNING = false;
static std::atomic<bool> THREAD_RUNNING { false };
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
// positions use game-client pixels, matching the legacy client-sized touch window
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
{EAM_IO_KEYPAD_0, {760, 440}},
{EAM_IO_KEYPAD_1, {760, 385}},
{EAM_IO_KEYPAD_2, {820, 385}},
@@ -31,7 +33,8 @@ namespace games::nost::poke {
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
};
static const std::unordered_map<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 +49,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() {
@@ -65,124 +63,138 @@ namespace games::nost::poke {
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300;
const int swipe_next_page_x_begin = 1120;
const int swipe_next_page_x_end = 1120-120;
const int swipe_next_page_x_end = 1120 - 120;
const int swipe_prev_page_x_begin = 280-120;
const int swipe_prev_page_x_begin = 280 - 120;
const int swipe_prev_page_x_end = 280;
int next_page_anim_index = -1;
int prev_page_anim_index = -1;
std::vector<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"
+2 -12
View File
@@ -19,13 +19,11 @@
#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;
#if SPICE64 && !SPICE_XP
@@ -715,15 +713,7 @@ 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;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
}
}
nativetouch::hook(avs::game::DLL_INSTANCE);
sysutils::hook_EnumDisplayDevicesA();
-1
View File
@@ -22,7 +22,6 @@ namespace games::popn {
#endif
extern bool SHOW_PIKA_MONITOR_WARNING;
extern bool NATIVE_TOUCH;
class POPNGame : public games::Game {
public:
+2 -2
View File
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) {
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
}
// init stuff
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
log_info("rb", "not changing touch poll rate (~125Hz by default)");
}
}
-1
View File
@@ -7,7 +7,6 @@
namespace games::rb {
extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game {
+79 -50
View File
@@ -4,7 +4,10 @@
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/touch.h"
#include "touch/touch.h"
#include "touch_debug.h"
#include "touch_defs.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
@@ -12,28 +15,38 @@
static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb {
uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
static void packet_set_bit(unsigned char *packet, int bit) {
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
static_cast<unsigned char>(1u << (bit % 8));
}
}
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
this->log_fps = log_fps;
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
}
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
// read() contracts X uniformly so screen edges land within usable sensors 2..45
int grid_x = CLAMP(
(cursor_x * X_SENSOR_COUNT) / window_width,
0,
X_SENSOR_COUNT - 1);
int grid_y = CLAMP(
(cursor_y * Y_SENSOR_COUNT) / window_height,
0,
Y_SENSOR_COUNT - 1);
// get bit positions
int bit_x = 88 + grid_x;
int bit_y = 74 - grid_y;
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
// insert bits
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
packet_set_bit(data, bit_x);
packet_set_bit(data, bit_y);
}
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
if (wnd != nullptr) {
// cache the game window so read() can size against it regardless of focus
this->game_hwnd = wnd;
// reset window process to make the game not crash
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
// update window
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}
// create touch window
touch_create_wnd(wnd);
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else {
// create touch window
touch_create_wnd(wnd);
// create touch window
touch_create_wnd(wnd);
// show game window because it lost focus
// show fullscreen game window because it lost focus
if (!GRAPHICS_WINDOWED) {
ShowWindow(wnd, SW_SHOW);
}
} else {
// fallback to dx hook
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
ShowCursor(TRUE);
}
touch_debug_attach();
return true;
}
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
// check buffer size
if (nNumberOfBytesToRead < 20) {
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
return 0;
}
// get window
HWND window = GetForegroundWindow();
// size against the game window cached in open() (not GetForegroundWindow), so
// touch stays correctly scaled and keeps working when the game loses focus
HWND window = this->game_hwnd;
if (window == nullptr) {
return 0;
}
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// create data
unsigned char data[20] {};
unsigned char data[TOUCH_PACKET_SIZE] {};
// data header
data[0] = 0x55;
data[2] = 0x4C;
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
// iterate all touch points
auto offset_x = (int) (window_width / 48.0 / 3.0);
auto offset_y = (int) (window_height / 76.0 / 3.0);
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
for (auto &point : touch_points) {
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// apply scaling
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
x = x / SCALE_FACTOR;
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
y = y / SCALE_FACTOR;
x = x - (window_width * (1.f - scale_factor) / 2);
x = x / scale_factor;
y = y - (window_height * (1.f - scale_factor) / 2);
y = y / scale_factor;
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
continue;
}
// point scaling
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76);
// point coordinates
// keep the verified left edge fixed while adding up to one-third of a beam
// at the right edge, preventing the finger footprint from including sensor 44
const auto point_x = (int) (
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
window_width / 2.0 +
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
const auto point_y = (int) y;
// center
grid_insert(data, point_x, point_y);
// surrounding points
if (games::rb::TOUCH_SIZE == 3) {
grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
// model a finger as a 3x3 block of IR sensors around the touch point
// this gives better accuracy (than just 1x1) since the logic below
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
// the center point, which means by inserting up to 8 extra blocks
// we are emulating a sub-"pixel" resolution
grid_insert(data, point_x, point_y); // center
grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
touch_debug_publish(data, is_landscape);
// copy data to buffer
memcpy(lpBuffer, data, 20);
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
// update frame logging
if (log_fps) {
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// return amount of bytes written
return 20;
return TOUCH_PACKET_SIZE;
}
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
touch_debug_detach();
// detach touch module
touch_detach();
+6
View File
@@ -12,6 +12,12 @@ namespace games::rb {
int window_width = 1080;
int window_height = 1920;
// game window resolved in open(); read() sizes against this instead of the
// foreground window so touch stays correctly scaled and keeps working when
// the game is not the foreground window (rawinput delivers touch regardless
// of focus)
HWND game_hwnd = nullptr;
// logging
bool log_fps = false;
uint64_t log_time = 0;
+143
View File
@@ -0,0 +1,143 @@
#include "touch_debug.h"
#include <array>
#include <atomic>
#include <cstring>
#include <mutex>
#include "external/imgui/imgui.h"
#include "games/rb/rb.h"
#include "games/rb/touch_defs.h"
namespace games::rb {
struct TouchDebugState {
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
bool is_landscape = false;
};
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
static std::atomic_bool TOUCH_ACTIVE = false;
static std::mutex TOUCH_DEBUG_STATE_M;
static TouchDebugState TOUCH_DEBUG_STATE;
static float touch_scale_factor() {
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
}
static void clear_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
TOUCH_DEBUG_STATE = {};
}
static TouchDebugState get_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
return TOUCH_DEBUG_STATE;
}
static bool packet_bit_active(
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
}
static int sensor_center(int sensor, int sensor_count, int extent) {
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
}
static float sensor_span_position(int sensor, int sensor_count, int extent) {
return sensor * (extent - 1) / (float) (sensor_count - 1);
}
bool touch_debug_overlay_enabled() {
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
}
void touch_draw_debug_overlay() {
if (!touch_debug_overlay_enabled()) {
return;
}
const auto &io = ImGui::GetIO();
int width = static_cast<int>(io.DisplaySize.x);
int height = static_cast<int>(io.DisplaySize.y);
if (width <= 0 || height <= 0) {
return;
}
const float scale_factor = touch_scale_factor();
const float left = width * (1.f - scale_factor) / 2.f;
const float top = height * (1.f - scale_factor) / 2.f;
const float right = width - left;
const float bottom = height - top;
TouchDebugState state = get_touch_debug_state();
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
auto draw_line = [&](float x1, float y1, float x2, float y2) {
draw_list->AddLine(
ImVec2(x1, y1), ImVec2(x2, y2),
IM_COL32(0, 255, 64, 255), 2.f);
};
// show the valid input area when touch scaling restricts it
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
draw_list->AddRect(
ImVec2(left, top), ImVec2(right, bottom),
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
}
// spread the usable X sensors 2..45 from edge to edge
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
continue;
}
float position = sensor_span_position(
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
state.is_landscape ? height : width);
if (state.is_landscape) {
float y = top + position * scale_factor;
draw_line(left, y, right, y);
} else {
float x = left + position * scale_factor;
draw_line(x, top, x, bottom);
}
}
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
continue;
}
int position = sensor_center(
sensor, Y_SENSOR_COUNT,
state.is_landscape ? width : height);
if (state.is_landscape) {
float x = right - position * scale_factor;
draw_line(x, top, x, bottom);
} else {
float y = top + position * scale_factor;
draw_line(left, y, right, y);
}
}
}
void touch_debug_attach() {
clear_touch_debug_state();
TOUCH_ACTIVE.store(true, std::memory_order_release);
}
void touch_debug_detach() {
TOUCH_ACTIVE.store(false, std::memory_order_release);
clear_touch_debug_state();
}
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
if (!TOUCH_DEBUG_OVERLAY) {
return;
}
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <atomic>
namespace games::rb {
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
bool touch_debug_overlay_enabled();
void touch_draw_debug_overlay();
void touch_debug_attach();
void touch_debug_detach();
void touch_debug_publish(const unsigned char *data, bool is_landscape);
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
namespace games::rb {
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
inline constexpr int TOUCH_PACKET_SIZE = 20;
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
inline constexpr int X_SENSOR_COUNT = 48;
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
inline constexpr int X_SENSOR_ACTIVE_COUNT =
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
inline constexpr int X_SENSOR_FIRST_BIT = 88;
inline constexpr int Y_SENSOR_COUNT = 76;
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
}
+19 -16
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,7 @@
#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 +48,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,29 +429,31 @@ 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
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"SOUND VOLTEX",
"Main Screen",
avs::game::DLL_INSTANCE);
}
nativetouch::hook(avs::game::DLL_INSTANCE);
// insert BI2X hooks
bi2x_hook_init();
-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;
+19 -9
View File
@@ -32,18 +32,28 @@ namespace hooks::audio {
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
index++) {
// read description (display name), fall back to the key name
// read description (display name), fall back to the key name.
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
// because the latter is unavailable on Windows XP.
char desc[256] = { 0 };
DWORD size = sizeof(desc);
std::string name = key_name;
if (RegGetValueA(hkEnum,
key_name,
ASIO_REG_DESC,
RRF_RT_REG_SZ | wow64_flag,
nullptr,
desc,
&size) == ERROR_SUCCESS && desc[0]) {
name = desc;
HKEY hkDriver = nullptr;
if (RegOpenKeyExA(hkEnum, key_name, 0,
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
DWORD type = 0;
if (RegQueryValueExA(hkDriver,
ASIO_REG_DESC,
nullptr,
&type,
reinterpret_cast<LPBYTE>(desc),
&size) == ERROR_SUCCESS
&& type == REG_SZ && desc[0]) {
// ensure null termination
desc[sizeof(desc) - 1] = '\0';
name = desc;
}
RegCloseKey(hkDriver);
}
// merge with an existing entry from the other view (match by name)
+96 -43
View File
@@ -154,36 +154,30 @@ namespace {
}
}
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
// new instance without releasing the old one has leaked it; we track this to tear the
// stale one down
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
// is destroyed and re-created within a process - which the host triggers by leaking and
// re-instantiating the driver during startup probing. so we build the real driver and
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
// the same wrapper back for every later CoCreate. as a result the real driver is created
// and initialized exactly once
std::mutex g_wrappers_mutex;
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
// replaces so the caller can tear it down outside the lock
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
WrappedAsio *find_wrapper(REFCLSID clsid) {
std::lock_guard lock(g_wrappers_mutex);
for (auto &entry : g_wrappers) {
if (IsEqualCLSID(entry.first, clsid)) {
WrappedAsio *stale = entry.second;
entry.second = wrapper;
return stale;
return entry.second;
}
}
g_wrappers.emplace_back(clsid, wrapper);
return nullptr;
}
void unregister_wrapper(WrappedAsio *wrapper) {
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex);
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
if (it->second == wrapper) {
g_wrappers.erase(it);
return;
}
}
g_wrappers.emplace_back(clsid, wrapper);
}
// ASIO drivers registered on this system (CLSID + registry name), scanned once
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
}
WrappedAsio::~WrappedAsio() {
unregister_wrapper(this);
this->detach_post_process();
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
// exactly one here and this releases/unloads it deterministically
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
// only once the host has released its own references. tears down the real driver
this->pReal->Release();
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
#pragma region IAsio
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
// the real driver is single-instance and kept alive; initialize it exactly once. the
// host re-calls init() on each CoCreate during probing, but re-initializing a live
// driver crashes some hardware drivers, so once we have a live driver we report success
if (this->initialized) {
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
return AsioTrue;
}
const AsioBool result = this->pReal->init(sys_handle);
if (result == AsioTrue) {
this->initialized = true;
log_info(
"audio::wrappedasio",
"init succeeded for '{}' (driver version {})",
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
AsioError __thiscall WrappedAsio::start() {
const AsioError result = this->pReal->start();
if (result == ASE_OK) {
this->started = true;
log_info(
"audio::wrappedasio",
"start succeeded, ASIO stream is now running on '{}'",
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
AsioError __thiscall WrappedAsio::stop() {
const AsioError result = this->pReal->stop();
if (result == ASE_OK) {
this->started = false;
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
} else {
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
}
void WrappedAsio::quiesce_for_reuse() {
// a previous abandoned probing cycle may have left a running stream and live buffers on
// the real driver without calling stop/dispose; tear that down now (without destroying
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
// further buffer_switch, and dispose_buffers detaches our trampolines
if (this->started) {
log_info(
"audio::wrappedasio",
"reuse: stopping leftover stream on '{}' before handing the driver back",
this->driver_name);
this->stop();
}
if (this->buffers_created) {
log_info(
"audio::wrappedasio",
"reuse: disposing leftover buffers on '{}' before handing the driver back",
this->driver_name);
this->dispose_buffers();
}
}
void WrappedAsio::apply_output_volume(long double_buffer_index) {
if (double_buffer_index != 0 && double_buffer_index != 1) {
return;
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
}
}
this->publish_post_process(buffer_size);
this->buffers_created = true;
} else {
log_warning(
"audio::wrappedasio",
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
// device's 2.0 output. then publish ourselves to the realtime thread
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
this->publish_post_process(buffer_size);
this->buffers_created = true;
log_info(
"audio::wrappedasio",
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
this->volume_channels.clear();
this->volume_active = false;
this->downmix_active = false;
this->buffers_created = false;
return result;
}
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
auto *wrapper = new WrappedAsio(
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
// if the host already had a live wrapper for this CLSID it leaked the previous
// instance (ASIO is single-instance); tear it down now so the real driver is
// released before the reinit. works around games (iidx32+) that ref twice but
// deref once before re-initializing.
//
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
// Xonar AE can
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
log_info(
"audio::wrappedasio",
"host did not release previous instance for clsid={}, forcing teardown",
guid2s(clsid));
// the stale instance may still have a running stream on the driver's realtime
// thread. stop and dispose it before deleting so no in-flight buffer switch
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
// detaches our trampolines, fully quiescing the realtime path before delete
stale->stop();
stale->dispose_buffers();
delete stale;
}
// pin the wrapper (and the real driver it owns) for the process lifetime; later
// CoCreate calls reuse this same instance via wrap_existing
wrapper->AddRef();
store_wrapper(clsid, wrapper);
return static_cast<IAsio *>(wrapper);
}
IUnknown *wrap_existing(REFCLSID clsid) {
WrappedAsio *wrapper = find_wrapper(clsid);
if (wrapper == nullptr) {
return nullptr;
}
log_misc(
"audio::wrappedasio",
"reusing cached ASIO driver instance, clsid={}",
guid2s(clsid));
wrapper->quiesce_for_reuse();
// hand the host another reference to the one instance we keep alive
wrapper->AddRef();
return static_cast<IAsio *>(wrapper);
}
void release_all_wrappers() {
// collect the pinned wrappers under the lock, then drop our references outside it so
// a final Release (which can run the driver's own teardown) never happens while the
// lock is held
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
{
std::lock_guard lock(g_wrappers_mutex);
wrappers = g_wrappers;
g_wrappers.clear();
}
for (auto &entry : wrappers) {
// drop only the pin wrap() took. if the host has already released its own
// references (the normal case at shutdown) this destroys the wrapper and releases
// the real driver; if the host is still using it, the refcount stays above zero
// and we leave its live stream/buffers untouched
log_misc(
"audio::wrappedasio",
"releasing cached ASIO driver instance, clsid={}",
guid2s(entry.first));
entry.second->Release();
}
}
}
+25 -1
View File
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
// return a proxy that forwards every call to it
// return a proxy that forwards every call to it. also records it as the cached
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
IUnknown *wrap(REFCLSID clsid, void *real);
// if a cached wrapper already exists for this CLSID, return it (with an added
// reference); otherwise nullptr to signal the caller to create the real driver and
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
IUnknown *wrap_existing(REFCLSID clsid);
// drop the process-lifetime references taken by wrap() so cached drivers can be released
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
// has released its own references too. call from a controlled shutdown point, never from
// a static destructor (the driver DLL may already be unloaded)
void release_all_wrappers();
}
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
AsioError __thiscall output_ready() override;
#pragma endregion
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
// for reuse, without destroying the real driver (see wrap_existing)
void quiesce_for_reuse();
private:
// create_buffers implementation used when a stereo extraction is active: forwards only
// the channels the real device has and hands the game throwaway buffers for the rest
@@ -170,6 +186,14 @@ private:
// unambiguous name; constant for our lifetime
std::string driver_name;
// the real driver is initialized exactly once; repeat init() calls are a no-op success
bool initialized = false;
// whether the real driver currently has a buffer set / running stream. used to quiesce
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
bool buffers_created = false;
bool started = false;
// our own reference count; we hold one reference on pReal and release it when this
// drops to zero
std::atomic<ULONG> ref_count {1};
+59 -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(
@@ -68,6 +100,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
HRESULT ret;
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
*ppv = reused;
return S_OK;
}
}
// call original
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) {
@@ -120,15 +161,28 @@ 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
// at a controlled shutdown point (see asio_proxy.cpp)
hooks::audio::asio::release_all_wrappers();
}
}
+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();
}
+29 -26
View File
@@ -23,12 +23,12 @@
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_indicators.h"
#include "touch/touch_gestures.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/fileutils.h"
#include "util/utils.h"
#include "misc/wintouchemu.h"
#include "touch/native/inject.h"
#include "util/time.h"
#include "rawinput/rawinput.h"
@@ -188,11 +188,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;
@@ -305,22 +307,6 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
}
}
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used
switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return true;
}
}
// window resize
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
@@ -589,6 +575,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 +601,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);
@@ -627,7 +616,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
}
disable_touch_indicators(result);
disable_touch_gestures(result);
log_misc(
"graphics",
"CreateWindowExA returned {}, {}",
@@ -722,7 +711,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
fmt::ptr(result),
lpWindowName ? ws2s(lpWindowName) : "(null)");
disable_touch_indicators(result);
disable_touch_gestures(result);
return result;
}
@@ -797,7 +786,7 @@ 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);
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
} else {
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
log_info(
@@ -950,6 +939,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 +1159,13 @@ 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::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(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);
+58 -28
View File
@@ -49,6 +49,7 @@
#include "games/popn/popn.h"
#include "games/qma/qma.h"
#include "games/rb/rb.h"
#include "games/rb/touch_debug.h"
#include "games/rf3d/rf3d.h"
#include "games/sc/sc.h"
#include "games/scotto/scotto.h"
@@ -101,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"
@@ -378,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;
@@ -427,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
@@ -489,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();
@@ -592,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)) {
@@ -642,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;
}
@@ -888,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()) {
@@ -1064,6 +1054,15 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
rawinput::touch::INVERTED = true;
}
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
if (mode == "on") {
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
} else if (mode == "off") {
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
}
// "auto" leaves the default (per-game)
}
// DisableTouchCardInsert is no longer honored in spice2x
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
// SPICETOUCH_CARD_DISABLE = true;
@@ -1105,6 +1104,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::DisableOverlay].value_bool()) {
overlay::ENABLED = false;
}
if (options[launcher::Options::OverlayKeyboardNavigation].value_bool()) {
overlay::ENABLE_KEYBOARD_NAVIGATION = true;
}
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
if (10 <= val && val <= 400 && val != 100) {
@@ -1273,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()) {
@@ -1305,21 +1315,36 @@ int main_implementation(int argc, char *argv[]) {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
} else {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
}
}
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
if (mode == "none") {
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
} else if (mode == "box") {
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
} else if (mode == "all") {
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
} else {
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
}
}
if (options[launcher::Options::JubeatTouchDebounce].is_active()) {
games::jb::TOUCH_DEBOUNCE_MS =
options[launcher::Options::JubeatTouchDebounce].value_uint32();
}
// reflec beat touch emulation
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
}
if (options[launcher::Options::RBTouchSize].is_active()) {
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
}
@@ -1761,7 +1786,16 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::poke::enable();
}
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");
}
}
}
@@ -2489,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();
@@ -2620,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);
+152 -37
View File
@@ -434,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Advanced Network",
},
{
.title = "Preferred NetAdapter IP",
.title = "Preferred Network Adapter's IP",
.name = "network",
.desc = "This is NOT the EA service URL; use -url for that. "
"Force the use of an adapter with the specified network. Must also provide -subnet.",
.display_name = "netadapterip",
.aliases = "netadapterip",
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
"with the specified IP address. You must also set -netadaptersubnet.",
.type = OptionType::Text,
.category = "Advanced Network",
.sensitive = true,
},
{
.title = "Preferred NetAdapter Subnet",
.title = "Preferred Network Adapter's Subnet",
.name = "subnet",
.desc = "Force the use of an adapter with the specified subnet. Must also provide -network.",
.display_name = "netadaptersubnet",
.aliases = "netadaptersubnet",
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
"with the specified subnet. You must also set -netadapterip.",
.type = OptionType::Text,
.category = "Advanced Network",
},
@@ -505,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.category = "General Overlay",
},
{
.title = "Overlay Keyboard Navigation",
.name = "keyboardnav",
.desc = "Enables keyboard navigation in the in-game overlay.",
.type = OptionType::Bool,
.category = "General Overlay",
},
{
// OverlayScaling
.title = "Spice Overlay UI Scale %",
@@ -950,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",
},
@@ -1031,16 +1042,16 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
.name = "sdvxnolive2d",
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
"Off: leave Live2D as-is.\n"
"Always: never render Live2D (also removes the menu navigator).\n"
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
"off: leave Live2D as-is.\n"
"always: hide Live2D (also hides menu navigator).\n"
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
.type = OptionType::Enum,
.game_name = "Sound Voltex",
.category = "Advanced Game Options",
.elements = {
{"off", "Show Live2D"},
{"always", "Never show Live2D"},
{"ingame", "Show navigators only"},
{"always", "Hide Live2D"},
{"ingame", "Hide during songs"},
},
.quick_setting_category = "Game",
},
@@ -1147,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",
},
@@ -1772,11 +1782,11 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "I/O Modules",
},
{
.title = "Disable Raw Input Touch",
.title = "Touch Compatibility Mode (Disable Raw Input Touch)",
.name = "wintouch",
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
"Win8 Pointer API or Win7 Touch API. Only enable this if you have trouble "
"using the default (raw input) touch input, as Raw Input performs better.",
"Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
"touch screens, but may result in worse performance and higher latency.",
.type = OptionType::Bool,
.category = "Touch Parameters",
},
@@ -1796,6 +1806,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.category = "Touch Parameters",
},
{
// RawInputTouchAspectRatio
.title = "Raw Input Touch Fix Aspect Ratio",
.name = "rawtouchaspect",
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
"different aspect ratio than the touch panel's native resolution. Only affects the default "
"raw input touch handler. This is automatically enabled, only force on or off if you have trouble.\n\n"
"auto (default): automatically enable on a per-game basis.\n"
"on: forced on\n"
"off: forced off",
.type = OptionType::Enum,
.category = "Touch Parameters",
.elements = {
{"auto", ""},
{"on", ""},
{"off", ""},
},
},
{
// DisableTouchCardInsert
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
@@ -1807,12 +1835,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// spice2x_TouchCardInsert
.title = "Show Insert Card button",
.title = "Show Insert Card button (DEPRECATED)",
.name = "sp2x-touchcard",
.display_name = "touchcard",
.aliases= "touchcard",
.desc = "Show Insert Card touch button on main display.",
.desc = "Show Insert Card touch button on main display. "
"DEPRECATED - only works in very specific situations (jubeat + wintouch)",
.type = OptionType::Bool,
.hidden = true,
.category = "Touch Parameters",
},
{
@@ -1884,7 +1914,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "HID SmartCard Fix UID",
.name = "scardfix",
.desc = "Modify behavior of SmartCard UID logic.\n\n"
"legacy: Preserve buggy old behavior (Default)\n\n"
"legacy (default): Preserve buggy old behavior\n\n"
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
@@ -2612,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",
@@ -2668,6 +2718,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
.type = OptionType::Enum,
.game_name = "Jubeat",
@@ -2675,10 +2726,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.elements = {
{"legacy", ""},
{"improved", ""},
{"plus", ""},
{"accurate", ""},
},
.quick_setting_category = "Game",
},
{
// JubeatTouchDebug
.title = "JB Touch Debug Overlay",
.name = "jubeattouchdebug",
.desc = "For touch screen players: draw a debug overlay on the main display. "
"Requires the Spice Overlay to be enabled.\n\n"
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
"none - draw nothing\n\n"
"box - show the 4x4 touch boundary boxes\n\n"
"all - show both the boxes and the touch circles",
.type = OptionType::Enum,
.game_name = "Jubeat",
.category = "Game Options",
.elements = {
{"auto", ""},
{"none", ""},
{"box", ""},
{"all", ""},
},
.quick_setting_category = "Game",
},
{
// JubeatTouchDebounce
.title = "JB Touch Debounce",
.name = "jubeattouchdebounce",
.desc = "For touch screen players: ignore extremely quick touches by requiring a "
"touch to be held for at least this many milliseconds before it registers.\n\n"
"Useful for filtering phantom touches on a noisy touch screen. A higher value "
"adds input latency, so keep it small. Default: off (0).",
.type = OptionType::Integer,
.setting_name = "8",
.game_name = "Jubeat",
.category = "Game Options",
.quick_setting_category = "Game",
},
{
// RBTouchDebug
.title = "RB Touch Debug Overlay",
.name = "rbtouchdebug",
.desc = "Draw lines to show IR sensor emulation state.\n\n"
"Note: lines will not perfectly align with touches; this is by design, it's showing the "
"IR sensor state, before the game applies its own touch detection algorithm.",
.type = OptionType::Bool,
.game_name = "Reflec Beat",
.category = "Game Options",
.quick_setting_category = "Game",
},
{
// spice2x_RBTouchScale
.title = "RB Touch Emulation Scale",
@@ -2693,14 +2792,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// RBTouchSize
.title = "RB Touch Emulation Size",
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
.name = "rbtouchsize",
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
.desc = "This option is deprecated and no longer has any effect. "
"Reflec Beat touch emulation always uses the 3x3 sensor model.",
.type = OptionType::Enum,
.hidden = true,
.game_name = "Reflec Beat",
.category = "Game Options",
.elements = {
{"1", "1x1"},
{"3", "3x3"},
},
},
@@ -2708,9 +2808,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// RBTouchPollRate
.title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
.desc = "By default, the game polls for touch at ~125Hz. "
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
"Higher rates reduce input latency (the game sees a fresher touch position) "
"but do NOT improve spatial accuracy, and very high rates just waste CPU.",
.type = OptionType::Integer,
.setting_name = "250",
.game_name = "Reflec Beat",
@@ -2787,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",
},
@@ -3049,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
+8
View File
@@ -53,6 +53,7 @@ namespace launcher {
SOFTID,
VREnable,
DisableOverlay,
OverlayKeyboardNavigation,
OverlayScaling,
NotificationPosition,
spice2x_FpsAutoShow,
@@ -179,6 +180,7 @@ namespace launcher {
ForceWinTouch,
ForceTouchEmulation,
InvertTouchCoordinates,
RawInputTouchAspectRatio,
DisableTouchCardInsert,
spice2x_TouchCardInsert,
ICCAReaderPort,
@@ -256,12 +258,17 @@ namespace launcher {
GitaDoraWindowedLeftMonitor,
GitaDoraWindowedRightMonitor,
GitaDoraWindowedSmallMonitor,
GitaDoraWindowedSmallSize,
GitaDoraWindowedSmallPosition,
spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition,
IIDXWindowedSubscreenBorderless,
IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
JubeatTouchDebug,
JubeatTouchDebounce,
RBTouchDebug,
spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
@@ -290,6 +297,7 @@ namespace launcher {
DDRP4IOBufferMode,
InputRequiresFocus,
NostalgiaPoke,
NostalgiaTouchMode,
ForceBackBufferCount,
SDVXWindowedSubscreenSize,
SDVXWindowedSubscreenPosition,
+34 -32
View File
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
// update touch
games::jb::touch_update();
auto touched = games::jb::touch_state();
// get buttons
auto &buttons = games::jb::get_buttons();
@@ -382,52 +383,52 @@ static void __cdecl device_update() {
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
DEVICE_INPUT_STATE |= 1 << 29;
}
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
DEVICE_INPUT_STATE |= 1 << 13;
}
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
DEVICE_INPUT_STATE |= 1 << 9;
}
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
DEVICE_INPUT_STATE |= 1 << 21;
}
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
DEVICE_INPUT_STATE |= 1 << 17;
}
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
DEVICE_INPUT_STATE |= 1 << 14;
}
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
DEVICE_INPUT_STATE |= 1 << 10;
}
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
DEVICE_INPUT_STATE |= 1 << 22;
}
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
DEVICE_INPUT_STATE |= 1 << 18;
}
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
DEVICE_INPUT_STATE |= 1 << 15;
}
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
DEVICE_INPUT_STATE |= 1 << 11;
}
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
DEVICE_INPUT_STATE |= 1 << 23;
}
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
DEVICE_INPUT_STATE |= 1 << 19;
}
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
DEVICE_INPUT_STATE |= 1 << 24;
}
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
DEVICE_INPUT_STATE |= 1 << 12;
}
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
DEVICE_INPUT_STATE |= 1 << 26;
}
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
DEVICE_INPUT_STATE |= 1 << 20;
}
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
// update touch
games::jb::touch_update();
auto touched = games::jb::touch_state();
// get buttons
auto &buttons = games::jb::get_buttons();
@@ -455,52 +457,52 @@ static void __cdecl device_update() {
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
DEVICE_INPUT_STATE |= 1 << 24;
}
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
DEVICE_INPUT_STATE |= 1 << 5;
}
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
DEVICE_INPUT_STATE |= 1 << 1;
}
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
DEVICE_INPUT_STATE |= 1 << 13;
}
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
DEVICE_INPUT_STATE |= 1 << 9;
}
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
DEVICE_INPUT_STATE |= 1 << 6;
}
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
DEVICE_INPUT_STATE |= 1 << 2;
}
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
DEVICE_INPUT_STATE |= 1 << 14;
}
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
DEVICE_INPUT_STATE |= 1 << 10;
}
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
DEVICE_INPUT_STATE |= 1 << 7;
}
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
DEVICE_INPUT_STATE |= 1 << 3;
}
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
DEVICE_INPUT_STATE |= 1 << 15;
}
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
DEVICE_INPUT_STATE |= 1 << 11;
}
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
DEVICE_INPUT_STATE |= 1 << 16;
}
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
DEVICE_INPUT_STATE |= 1 << 4;
}
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
DEVICE_INPUT_STATE |= 1 << 20;
}
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
DEVICE_INPUT_STATE |= 1 << 12;
}
}
-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);
}
+9 -3
View File
@@ -20,6 +20,7 @@ static cardunit_card_cardnumber_t cardunit_card_cardnumber = nullptr;
static HINSTANCE SCIUNIT_INSTANCE;
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
static bool SCIUNIT_INITIALIZED = false;
static int SCIUNIT_STATUS = 0;
static bool CARD_IN = false;
static bool CARD_PRESSED = false;
static uint8_t CARD_UID[8];
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
}
static int __cdecl sciunit_get_errorunit() {
return 0;
return -1;
}
static int __cdecl sciunit_get_stat() {
return 0;
return SCIUNIT_STATUS;
}
static int __cdecl sciunit_get_version(int a1, int a2) {
@@ -163,6 +164,7 @@ static int __cdecl sciunit_get_version(int a1, int a2) {
static int __cdecl sciunit_initialize() {
SCIUNIT_INITIALIZED = true;
SCIUNIT_STATUS = 1;
return 0;
}
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
}
static int __cdecl sciunit_update() {
if (SCIUNIT_INITIALIZED)
if (SCIUNIT_INITIALIZED) {
if (SCIUNIT_STATUS == 1) {
SCIUNIT_STATUS = 2;
}
update_card();
}
return 0;
}
+11 -236
View File
@@ -4,44 +4,19 @@
#include "wintouchemu.h"
#include <chrono>
#include <algorithm>
#include <optional>
#include <thread>
#include "cfg/screen_resize.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "touch/touch.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
namespace wintouchemu {
typedef struct {
POINT pos;
bool last_button_pressed;
DWORD touch_event;
} mouse_state_t;
// settings
bool FORCE = false;
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
}
@@ -81,16 +56,13 @@ namespace wintouchemu {
// state
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
bool USE_MOUSE = false;
std::vector<TouchEvent> TOUCH_EVENTS;
std::vector<TouchPoint> TOUCH_POINTS;
HMODULE HOOKED_MODULE = nullptr;
std::string WINDOW_TITLE_START = "";
std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
volatile bool INITIALIZED = false;
mouse_state_t mouse_state;
bool INITIALIZED = false;
void hook(const char *window_title, HMODULE module, int delay_in_s) {
void hook(const char *window_title, HMODULE module) {
// hooks
auto system_metrics_hook = detour::iat_try(
@@ -109,31 +81,8 @@ namespace wintouchemu {
// set module and title
HOOKED_MODULE = module;
WINDOW_TITLE_START = window_title;
if (0 < delay_in_s) {
// some games crash when touch events are injected too early during boot
std::thread t([&]() {
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
log_misc("wintouchemu", "initializing", delay_in_s);
INITIALIZED = true;
});
t.detach();
} else {
log_misc("wintouchemu", "initializing");
INITIALIZED = true;
}
}
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
hook(window_title_start, module);
WINDOW_TITLE_END = window_title_end;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
log_misc("wintouchemu", "initializing");
INITIALIZED = true;
}
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
@@ -145,7 +94,6 @@ namespace wintouchemu {
// set touch inputs
bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input];
@@ -178,13 +126,7 @@ namespace wintouchemu {
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
if (GRAPHICS_IIDX_WSUB) {
// touch was received on subscreen window.
RECT clientRect {};
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
} else if (overlay::OVERLAY) {
if (overlay::OVERLAY) {
// touch was received on global coords
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else {
@@ -230,89 +172,10 @@ namespace wintouchemu {
touch_input->cxContact = 0;
touch_input->cyContact = 0;
if (avs::game::is_model("KFC") &&
rawinput::touch::DISPLAY_INITIALIZED &&
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_touch_points(touch_input);
}
} else if (USE_MOUSE && !mouse_used) {
// disable further mouse inputs this call
mouse_used = true;
if (mouse_state.touch_event) {
result = true;
touch_input->x = mouse_state.pos.x;
touch_input->y = mouse_state.pos.y;
if (GRAPHICS_WINDOWED) {
touch_input->x -= SPICETOUCH_TOUCH_X;
touch_input->y -= SPICETOUCH_TOUCH_Y;
}
// log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true;
if (overlay::OVERLAY) {
valid = overlay::OVERLAY->transform_touch_point(
&touch_input->x, &touch_input->y);
}
// touch inputs require 100x precision per pixel
touch_input->x *= 100;
touch_input->y *= 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = 0;
touch_input->dwFlags = 0;
switch (mouse_state.touch_event) {
case TOUCHEVENTF_DOWN:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
}
break;
case TOUCHEVENTF_MOVE:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
}
break;
case TOUCHEVENTF_UP:
// don't check valid so that this touch ID can be released
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_UP;
break;
}
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
// reset it since the event was consumed & propagated as touch
mouse_state.touch_event = 0;
}
} else if (!GRAPHICS_IIDX_WSUB) {
/*
* For some reason, Nostalgia won't show an active touch point unless a move event
* triggers in the same frame. To work around this, we just supply a fake move
* event if we didn't update the same pointer ID in the same call.
*/
} else {
// beatstream requires a MOVE for active points in each update
// add one for every active point without a matching input event
// find touch point which has no associated input event
TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) {
@@ -380,21 +243,6 @@ namespace wintouchemu {
title = get_window_title(hWnd);
}
// if a window title end is set, check to see if it matches
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = nullptr;
title = "";
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
auto check_title = get_window_title(window);
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = std::move(window);
title = std::move(check_title);
break;
}
}
}
// check window
if (hWnd == nullptr) {
return;
@@ -402,39 +250,13 @@ namespace wintouchemu {
// check if windowed
if (GRAPHICS_WINDOWED) {
if (GRAPHICS_IIDX_WSUB) {
// no handling is needed here
// graphics::MoveWindow_hook will attach hook to windowed subscreen
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
USE_MOUSE = false;
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
// overlay subscreen in IIDX
// use mouse position as ImGui overlay will block the touch window
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
USE_MOUSE = true;
} else if (games::popn::is_pikapika_model()) {
// same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true;
} else {
// create touch window - create overlay if not yet existing at this point
log_info("wintouchemu", "create touch window relative to main game window");
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
USE_MOUSE = false;
}
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
log_info(
"wintouchemu",
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
USE_MOUSE = true;
// create touch window - create overlay if not yet existing at this point
log_info("wintouchemu", "create touch window relative to main game window");
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
} else {
log_info("wintouchemu", "activating DirectX hooks");
// mouse position based input only
touch_attach_dx_hook();
USE_MOUSE = false;
}
// hooks
@@ -445,8 +267,6 @@ namespace wintouchemu {
}
}
const auto now = get_performance_milliseconds();
// update touch events
if (hWnd != nullptr) {
@@ -466,56 +286,11 @@ namespace wintouchemu {
// check if new events are available
if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
}
// update frame logging
if (LOG_FPS) {
static int log_frames = 0;
static uint64_t log_time = 0;
log_frames++;
if (log_time < get_system_seconds()) {
if (log_time > 0) {
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
}
log_frames = 0;
log_time = get_system_seconds();
}
}
}
// send separate WM_TOUCH event for mouse
// this must be separate from actual touch events because some games will ignore the return
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
// send empty events when the mouse button is not clicked/released
if (hWnd != nullptr && USE_MOUSE) {
bool button_pressed = get_async_primary_mouse();
// if there was a touch event in the last 500 ms, don't insert new button presses
if (button_pressed && (now - last_touch_event) < 500) {
button_pressed = false;
}
// figure out what kind of touch event to simulate
if (button_pressed && !mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_DOWN;
} else if (button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_MOVE;
} else if (!button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_UP;
}
mouse_state.last_button_pressed = button_pressed;
if (mouse_state.touch_event) {
GetCursorPos(&mouse_state.pos);
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
}
}
}
}
+1 -7
View File
@@ -6,14 +6,8 @@ namespace wintouchemu {
// settings
extern bool FORCE;
extern bool INJECT_MOUSE_AS_WM_TOUCH;
extern bool LOG_FPS;
extern bool ADD_TOUCH_FLAG_PRIMARY;
void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0);
void hook_title_ends(
const char *window_title_start,
const char *window_title_ends,
HMODULE module = nullptr);
void hook(const char *window_title, HMODULE module = nullptr);
void update();
}
+38 -1
View File
@@ -5,7 +5,9 @@
#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"
#include "misc/eamuse.h"
#include "touch/touch.h"
@@ -50,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"
@@ -66,6 +69,7 @@ namespace overlay {
bool AUTO_SHOW_IOPANEL = false;
bool AUTO_SHOW_KEYPAD_P1 = false;
bool AUTO_SHOW_KEYPAD_P2 = false;
bool ENABLE_KEYBOARD_NAVIGATION = false;
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
std::optional<uint32_t> UI_SCALE_PERCENT;
@@ -311,6 +315,9 @@ void overlay::SpiceOverlay::init() {
auto &io = ImGui::GetIO();
io.UserData = this;
io.ConfigFlags = ImGuiConfigFlags_None;
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
}
if (!cfg::CONFIGURATOR_STANDALONE) {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
}
@@ -399,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);
@@ -551,9 +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) {
if (!this->active && !draw_notifications && !draw_fps_persistent &&
!draw_nostalgia_touch_piano && !draw_rb_touch_debug) {
return;
}
@@ -578,12 +600,22 @@ void overlay::SpiceOverlay::new_frame() {
ImGui::NewFrame();
this->has_pending_frame = true;
if (draw_rb_touch_debug) {
games::rb::touch_draw_debug_overlay();
}
// build windows only when the overlay itself is active
if (this->active) {
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();
@@ -595,6 +627,7 @@ void overlay::SpiceOverlay::new_frame() {
if (draw_fps_persistent) {
this->window_fps->build();
}
// draw notifications last so they paint on top of any overlay windows
if (draw_notifications) {
overlay::notifications::draw();
@@ -805,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();
+5
View File
@@ -44,6 +44,7 @@ namespace overlay {
extern bool AUTO_SHOW_IOPANEL;
extern bool AUTO_SHOW_KEYPAD_P1;
extern bool AUTO_SHOW_KEYPAD_P2;
extern bool ENABLE_KEYBOARD_NAVIGATION;
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
extern FpsLocation FPS_LOCATION;
extern bool SHOW_DEBUG_LOG_WINDOW;
@@ -82,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
@@ -137,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
};
}
+46 -19
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->devices_scan_midi();
// 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,9 +129,13 @@ 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
log_misc("hotplug", "device interface removal: {}", name);
this->ri_mgr->devices_remove(name);
}
}
@@ -120,8 +144,11 @@ namespace rawinput {
return TRUE;
}
case DBT_DEVNODES_CHANGED: {
// TODO: this can be a little noisy as it gets called on every device
this->ri_mgr->devices_scan_midi();
// catch-all device-tree change: MIDI and XInput have no targeted
// arrival/removal notification, so this is our only hook to detect them
// can be a little noisy as it gets called on every device
log_misc("hotplug", "device tree changed, rescanning MIDI + XInput");
this->ri_mgr->midi_scan_start();
this->ri_mgr->devices_scan_xinput();
break;
}
+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);
+726
View File
@@ -0,0 +1,726 @@
#include "rawinput.h"
#include <sstream>
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
namespace rawinput {
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
}
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
return rawinput::MIDI_NOTE_ALGORITHM;
}
void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
rawinput::MIDI_NOTE_ALGORITHM = new_algo;
std::string s = "Unknown";
switch (new_algo) {
case rawinput::MidiNoteAlgorithm::LEGACY:
s = "legacy";
break;
case rawinput::MidiNoteAlgorithm::V2:
s = "v2";
break;
case rawinput::MidiNoteAlgorithm::V2_DRUM:
s = "v2_drum";
break;
default:
log_info("rawinput", "assert failed: invalid midi algorithm");
break;
}
log_info("rawinput", "using MIDI algorithm: {}", s);
}
void rawinput::RawInputManager::midi_scan_start() {
// single-flight: only one scan runs at a time. if one is already running, set
// the pending flag so it rescans once more when it finishes - MIDI hotplug
// events fire while the slow enumeration is still going and must not be lost.
// the scheduler mutex makes this check-and-set atomic with the worker's
// exit-or-rescan decision below, so a request set while a scan is running is
// never dropped
{
std::lock_guard<std::mutex> lock(this->midi_scan_m);
if (this->midi_scan_active) {
this->midi_scan_pending = true;
log_misc("rawinput", "MIDI scan already running, queued rescan");
return;
}
this->midi_scan_active = true;
this->midi_scan_pending = false;
}
// clean up the previous (already finished) scan thread handle
this->midi_scan_join();
// run the (potentially slow) MIDI enumeration on its own thread so callers are
// not blocked while the Windows MIDI subsystem starts up. rescan if a request
// arrived while we were scanning
log_misc("rawinput", "starting async MIDI scan thread");
this->midi_thread = new std::thread([this]() {
for (;;) {
this->devices_scan_midi();
// decide whether to exit under the scheduler lock, atomically with any
// concurrent midi_scan_start(): if a rescan was requested, consume it
// and loop; otherwise clear active and exit. because both sides take
// the same lock, a request set while active is true is never lost, so
// we never strand a hotplug event waiting for a future one
std::lock_guard<std::mutex> lock(this->midi_scan_m);
if (!this->midi_scan_pending) {
this->midi_scan_active = false;
log_misc("rawinput", "async MIDI scan thread finished");
return;
}
this->midi_scan_pending = false;
log_misc("rawinput", "async MIDI scan rescanning (event arrived during scan)");
}
});
}
void rawinput::RawInputManager::midi_scan_join() {
if (this->midi_thread) {
if (this->midi_thread->joinable()) {
// this blocks until the scan worker returns. if it ever hangs here the
// worker is stuck - most likely in midi_close_deferred_flush() waiting
// on a WinMM close. a missing "joined" line pinpoints the hang
log_misc("rawinput", "joining MIDI scan thread...");
this->midi_thread->join();
log_misc("rawinput", "MIDI scan thread joined");
}
delete this->midi_thread;
this->midi_thread = nullptr;
}
}
void rawinput::RawInputManager::midi_close_deferred_flush() {
// take the queued handles under the lock, then close them without it. WinMM
// midiInReset/midiInClose block until in-flight input_midi_proc callbacks
// return, and those callbacks take devices_mutex, so closing under the lock
// would deadlock
std::vector<HMIDIIN> handles;
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
handles.swap(this->midi_close_deferred);
}
if (handles.empty()) {
return;
}
// if a hang is ever reported here it is the classic WinMM deadlock: an
// in-flight input_midi_proc callback is blocked on devices_mutex while
// midiInReset/midiInClose waits for that callback to return. the per-handle
// log below pinpoints exactly which close did not come back
log_misc("rawinput", "closing {} deferred MIDI handle(s)", handles.size());
for (size_t i = 0; i < handles.size(); i++) {
log_misc("rawinput", "closing deferred MIDI handle {}/{}", i + 1, handles.size());
midiInReset(handles[i]);
midiInClose(handles[i]);
}
log_misc("rawinput", "deferred MIDI handles closed");
}
void rawinput::RawInputManager::devices_scan_midi() {
log_misc("rawinput", "scan MIDI devices...");
// note: the WinMM MIDI calls below (midiInGetNumDevs / midiInGetDevCaps /
// midiInOpen / midiInStart) can block for seconds while the Windows MIDI
// subsystem starts up, so they must NOT run under devices_mutex. only the
// list mutation at the end of each iteration is guarded.
// identifiers of every MIDI device seen in this scan; used below to
// tombstone devices that have since been unplugged
std::vector<std::string> present_identifiers;
// add midi devices
auto midi_device_count = midiInGetNumDevs();
for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) {
// get dev caps
MIDIINCAPS midi_device_caps{};
if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) {
continue;
}
log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}",
midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid);
// build identifier for MIDI
// ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file)
// so it should never be changed
std::ostringstream midi_identifier_stream;
midi_identifier_stream << ";" << "MIDI";
midi_identifier_stream << ";" << midi_device_id;
midi_identifier_stream << ";" << midi_device_caps.szPname;
midi_identifier_stream << ";" << midi_device_caps.wMid;
midi_identifier_stream << ";" << midi_device_caps.wPid;
const auto midi_identifier = midi_identifier_stream.str();
// record that this device is currently present
present_identifiers.push_back(midi_identifier);
// if already open, leave it alone: hotplug fires many change events, and
// reopening on every rescan would drop the WinMM handle (and its input).
// only (re)open when the device is missing or a destroyed tombstone
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
bool already_open = false;
for (auto &device : this->devices) {
if (device.type == MIDI && device.name == midi_identifier) {
already_open = true;
break;
}
}
if (already_open) {
continue;
}
}
// open device
HMIDIIN midi_device_handle;
if (midiInOpen(&midi_device_handle,
(UINT) midi_device_id,
(DWORD_PTR) &input_midi_proc,
(DWORD_PTR) this,
CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
{
continue;
}
// start input
if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) {
// close the handle we just opened so it does not leak on repeated rescans
midiInClose(midi_device_handle);
continue;
}
// device info
DeviceInfo midi_device_info {};
// device midi info
auto midi_device_midi_info = new DeviceMIDIInfo();
midi_device_midi_info->states = std::vector<bool>(16 * 128);
midi_device_midi_info->states_events = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->bind_states = std::vector<bool>(16 * 128);
midi_device_midi_info->v2_last_on_time = std::vector<double>(16 * 128);
midi_device_midi_info->v2_last_off_time = std::vector<double>(16 * 128);
midi_device_midi_info->v2_velocity_threshold = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector<bool>(16 * 128);
midi_device_midi_info->velocity = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->freeze = false;
midi_device_midi_info->controls_precision = std::vector<uint16_t>(16 * 32);
midi_device_midi_info->controls_precision_bind = std::vector<uint16_t>(16 * 32);
midi_device_midi_info->controls_precision_msb = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_precision_lsb = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_precision_set = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_single = std::vector<uint8_t>(16 * 44);
midi_device_midi_info->controls_single_bind = std::vector<uint8_t>(16 * 44);
midi_device_midi_info->controls_single_set = std::vector<bool>(16 * 44);
midi_device_midi_info->controls_onoff = std::vector<bool>(16 * 6);
midi_device_midi_info->controls_onoff_bind = std::vector<bool>(16 * 6);
midi_device_midi_info->controls_onoff_set = std::vector<bool>(16 * 6);
midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector<double>(16 * 6);
midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector<double>(16 * 6);
midi_device_midi_info->pitch_bend = std::vector<int16_t>(16 * 6);
midi_device_midi_info->pitch_bend_set = std::vector<bool>(16 * 6);
// build device
Device midi_device {};
midi_device.type = MIDI;
midi_device.handle = midi_device_handle;
midi_device.name = midi_identifier;
midi_device.desc = to_string(midi_device_caps.szPname);
midi_device.info = midi_device_info;
midi_device.midiInfo = midi_device_midi_info;
// mutate the shared device list under lock (the slow WinMM calls above
// ran without it so other threads were not blocked)
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
midi_device.id = devices.size() + 1;
// reuse a previously destroyed tombstone with the same identifier, if any.
// (a live device with this identifier was already skipped above)
bool replaced = false;
for (auto &device : this->devices) {
if (device.name == midi_identifier) {
// carry over ID
midi_device.id = device.id;
// destruct and replace the slot in place under its locks
this->devices_destruct(&device);
replace_device_slot(device, midi_device);
// notify change
for (auto &cb : this->callback_change) {
cb.f(cb.data, &device);
}
replaced = true;
break;
}
}
if (replaced) {
continue;
}
// 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
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
}
// tombstone MIDI devices that were open but are no longer present (unplugged).
// otherwise a replugged device matches the stale live entry in the skip check
// above and never gets reopened, silently losing its input
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
for (auto &device : this->devices) {
if (device.type != MIDI) {
continue;
}
bool present = false;
for (const auto &identifier : present_identifiers) {
if (identifier == device.name) {
present = true;
break;
}
}
if (!present) {
log_info("rawinput", "MIDI device unplugged, releasing: {}", device.desc);
this->devices_destruct(&device);
}
}
}
// close the MIDI handles detached above, now that devices_mutex is released
this->midi_close_deferred_flush();
log_misc("rawinput", "scan MIDI devices done ({} enumerated)", (unsigned) midi_device_count);
}
void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance,
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
// get instance
auto ri_mgr = reinterpret_cast<RawInputManager *>(dwInstance);
// handle message
switch (wMsg) {
case MIM_OPEN:
case MIM_CLOSE:
break;
case MIM_MOREDATA:
case MIM_DATA: {
// lock the device list so a concurrent scan can't mutate it while we iterate
std::lock_guard<std::recursive_mutex> devices_lock(ri_mgr->devices_mutex);
// param mapping
auto dwMidiMessage = dwParam1;
//auto dwTimestamp = dwParam2;
// message unpacking
auto midi_status = LOBYTE(LOWORD(dwMidiMessage));
auto midi_status_command = (midi_status & 0xF0u) >> 4u;
auto midi_status_channel = (midi_status & 0x0Fu);
auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage));
auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage));
// callbacks
for (auto &callback : ri_mgr->callback_midi) {
// find device
for (auto &device : ri_mgr->devices_get()) {
if (device.type == MIDI && device.handle == hMidiIn) {
// call function
callback.f(callback.data, &device,
midi_status_command, midi_status_channel,
midi_byte1, midi_byte2);
}
}
}
// skip unused messages types early for performance
bool skip = false;
switch (midi_status_command) {
case 0xA: // POLYPHONIC PRESSURE
case 0xC: // PROGRAM CHANGE
case 0xD: // CHANNEL PRESSURE
case 0xF: // SYSTEM EXCLUSIVE
skip = true;
break;
default:
break;
}
if (skip) {
break;
}
// find device
for (auto &device : ri_mgr->devices_get()) {
// filter non MIDI devices
if (device.type != MIDI) {
continue;
}
// filter wrong handles
if (device.handle != hMidiIn) {
continue;
}
// get input time
const auto input_time = get_performance_seconds();
// lock device
std::lock_guard<std::mutex> lock(*device.mutex);
// update hz
auto diff_time = input_time - device.input_time;
if (diff_time > 0.0001) {
device.input_hz = 1.f / diff_time;
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
device.input_time = input_time;
}
// command logic
switch (midi_status_command) {
case 0x8: { // NOTE OFF
// param mapping
const auto midi_note = midi_byte1 & 127u;
// log_misc("midi", "[{}] OFF", midi_note);
// get index
const auto midi_index = midi_status_channel * 128 + midi_note;
if (midi_index < 16 * 128) {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
// update velocity
device.midiInfo->velocity[midi_index] = 0;
// disable note
if (device.midiInfo->states_events[midi_index]) {
device.midiInfo->states[midi_index] = false;
}
device.updated = true;
} else {
// v2 logic
// exactly the same as NOTE ON with 0 velocity
// velocity is kept; api will ignore it if button is not pressed
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds();
device.updated = true;
}
// for v2_drum, NOTE OFF is ignored
}
}
break;
}
case 0x9: { // NOTE ON
// param mapping
const auto midi_note = midi_byte1 & 127u;
// per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF.
const auto midi_velocity = midi_byte2 & 127u;
// log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity);
// get index
const auto midi_index = midi_status_channel * 128 + midi_note;
if (midi_index < 16 * 128) {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
// update velocity
device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity;
if (midi_velocity) {
// update events (for legacy logic)
// how does this work? see the comment in api.cpp around the check for
// get_midi_algorithm() for an explanation
// so currently it's meant to be turned on
device.midiInfo->states[midi_index] = true;
// if its already on just increase it by one to turn it off
if (device.midiInfo->states_events[midi_index] % 2)
device.midiInfo->states_events[midi_index]++;
else
device.midiInfo->states_events[midi_index] += 2;
} else if (!device.midiInfo->freeze) {
// velocity 0 means turn it off
device.midiInfo->states[midi_index] = false;
}
device.updated = true;
} else {
// v2 logic
const auto now = get_performance_milliseconds();
auto threshold = device.midiInfo->v2_velocity_threshold[midi_index];
// when device is frozen (binding is happening) ignore the velocity threshold
// this allows users to bind keys even if the midi note is set to high threshold at
// rawinput layer, either from a previous binding that was cleared, or existing binding
// for another button
if (device.midiInfo->freeze) {
threshold = 0;
}
if (threshold < midi_velocity) {
device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity;
device.midiInfo->v2_last_on_time[midi_index] = now;
// disable holds and release all notes immediately
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) {
device.midiInfo->v2_last_off_time[midi_index] = now;
}
device.updated = true;
} else {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
// insufficient velocity ON == exactly the same as NOTE OFF
device.midiInfo->v2_last_off_time[midi_index] = now;
device.updated = true;
}
// for v2_drum, NOTE ON with insufficient velocity is ignored
}
}
}
break;
}
case 0xA: // POLYPHONIC PRESSURE
break; // skipped above (!)
case 0xB: { // CONTROL CHANGE
// param mapping
auto midi_control = midi_byte1 & 127;
auto midi_value = midi_byte2 & 127u;
// get index
auto channel_offset = midi_status_channel * 128;
auto midi_index = channel_offset + midi_control;
if (midi_index < 16 * 128) {
// continuous controller MSB
if (midi_control >= 0x00 && midi_control <= 0x1F) {
// update index
midi_index = midi_status_channel * 32 + midi_control;
device.midiInfo->controls_precision_set[midi_index] = true;
// check if MSB wasn't sent yet
if (!device.midiInfo->controls_precision_msb[midi_index]) {
device.midiInfo->controls_precision_msb[midi_index] = true;
// move LSB value to actual position
device.midiInfo->controls_precision[midi_index] >>= 7u;
}
// update MSB
auto tmp = device.midiInfo->controls_precision[midi_index];
tmp = (tmp & 127u) | midi_value << 7u;
if (!device.midiInfo->controls_precision_lsb[midi_index])
tmp = (tmp & (127u << 7u)) | midi_value;
if (device.midiInfo->controls_precision[midi_index] != tmp) {
device.midiInfo->controls_precision[midi_index] = tmp;
device.updated = true;
}
}
// continuous controller LSB
else if (midi_control >= 0x20 && midi_control <= 0x3F) {
// update index
midi_index = midi_status_channel * 32 + midi_control - 0x20;
device.midiInfo->controls_precision_set[midi_index] = true;
device.midiInfo->controls_precision_lsb[midi_index] = true;
// check for MSB flag
if (device.midiInfo->controls_precision_msb[midi_index]) {
// update LSB only
auto tmp = device.midiInfo->controls_precision[midi_index];
tmp &= 127u << 7u;
tmp |= midi_value;
if (device.midiInfo->controls_precision[midi_index] != tmp) {
device.midiInfo->controls_precision[midi_index] = tmp;
device.updated = true;
}
} else {
// cast to MSB
if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) {
device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value;
device.updated = true;
}
}
}
// on/off controls
else if (midi_control >= 0x40 && midi_control <= 0x45) {
// update index
midi_index = midi_status_channel * 6 + midi_control - 0x40;
device.midiInfo->controls_onoff_set[midi_index] = true;
// get on/off state
const auto onoff_state = midi_value >= 64;
// update device
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
if (device.midiInfo->controls_onoff[midi_index] != onoff_state) {
device.midiInfo->controls_onoff[midi_index] = onoff_state;
device.updated = true;
}
} else {
// v2 and v2_drum:
// unlike notes (drum pads), controls can send continuous ON signal
// therefore, check for rising and falling edges
const auto now = get_performance_milliseconds();
const auto previous_value = device.midiInfo->controls_onoff[midi_index];
if (!previous_value && onoff_state) {
device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now;
device.updated = true;
} else if (previous_value && !onoff_state) {
device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now;
device.updated = true;
}
device.midiInfo->controls_onoff[midi_index] = onoff_state;
}
}
// single byte controllers
else if (midi_control >= 0x46 && midi_control <= 0x5F) {
// update index
midi_index = midi_status_channel * 44 + midi_control - 0x46;
device.midiInfo->controls_single_set[midi_index] = true;
// update device
if (device.midiInfo->controls_single[midi_index] != midi_value) {
device.midiInfo->controls_single[midi_index] = midi_value;
device.updated = true;
}
}
// increment/decrement and parameter numbers
else if (midi_control >= 0x60 && midi_control <= 0x65) {
// skip
}
// undefined single-byte controllers
else if (midi_control >= 0x66 && midi_control <= 0x77) {
// update index
auto sbc_count = 0x5F - 0x46 + 1;
midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count;
device.midiInfo->controls_single_set[midi_index] = true;
// update device
if (device.midiInfo->controls_single[midi_index] != midi_value) {
device.midiInfo->controls_single[midi_index] = midi_value;
device.updated = true;
}
}
// channel mode messages
else if (midi_control >= 0x78 && midi_control <= 0x7F) {
switch (midi_control) {
case 0x78: // all sound off
break;
case 0x79: { // reset all controllers
for (int i = 0; i < 32; i++)
device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0;
for (int i = 0; i < 44; i++)
device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0;
for (int i = 0; i < 6; i++) {
const auto index = midi_status_channel * 6 + i;
device.midiInfo->controls_onoff[index] = false;
device.midiInfo->v2_controls_onoff_last_on_time[index] = 0;
device.midiInfo->v2_controls_onoff_last_off_time[index] = 0;
}
device.updated = true;
break;
}
case 0x7A: // local control on/off
break;
case 0x7B: // all notes off
case 0x7C: // omni mode off + all notes off
case 0x7D: // omni mode on + all notes off
case 0x7E: // mono mode on + poly off + all notes off
case 0x7F: // poly mode on + mono off + all notes off
for (int i = 0; i < 128; i++) {
// common
device.midiInfo->velocity[channel_offset + i] = 0;
device.midiInfo->bind_states[channel_offset + i] = false;
// legacy
device.midiInfo->states[channel_offset + i] = false;
device.midiInfo->states_events[channel_offset + i] = 0;
// v2
device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0;
device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0;
}
device.updated = true;
break;
default:
break;
}
break;
}
}
break;
}
case 0xC: // PROGRAM CHANGE
break; // skipped above (!)
case 0xD: // CHANNEL PRESSURE
break; // skipped above (!)
case 0xE: { // PITCH BENDING
// raw values range from [0, 0x3FFF] (16383)
// build value, centered around zero [-8192, 8191]
int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000;
// update device
if (device.midiInfo->pitch_bend[midi_status_channel] != value) {
device.midiInfo->pitch_bend[midi_status_channel] = value;
device.midiInfo->pitch_bend_set[midi_status_channel] = true;
device.updated = true;
}
break;
}
case 0xF: // SYSTEM EXCLUSIVE
break; // skipped above (!)
default:
break;
}
// don't iterate through the other devices
break;
}
break;
}
case MIM_LONGDATA:
case MIM_ERROR:
case MIM_LONGERROR:
break;
default:
break;
}
}
File diff suppressed because it is too large Load Diff
+48 -3
View File
@@ -3,6 +3,8 @@
#include <functional>
#include <thread>
#include <condition_variable>
#include <list>
#include <mutex>
#include <vector>
#include <windows.h>
@@ -10,6 +12,7 @@
#include "device.h"
#include "hotplug.h"
#include "rawinput_handles.h"
#include "rawinput/xinput.h"
#include "util/scope_guard.h"
@@ -63,10 +66,41 @@ namespace rawinput {
class RawInputManager {
private:
HotplugManager *hotplug;
std::vector<Device> devices;
HotplugManager *hotplug = nullptr;
// std::list (not std::vector) so adding/removing a device never invalidates
// pointers to other elements. devices_get() hands out pointers that escape the
// lock and are dereferenced later by pollers, so a concurrent scan must not
// relocate existing devices
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;
// guards the MIDI scan scheduler flags below. only held around the flag
// checks, never across the (slow) enumeration, so it makes "is a scan
// running?" and the worker's "exit or rescan?" a single atomic decision -
// a request can never slip into the gap between the two
std::mutex midi_scan_m;
// true while a scan worker thread is running. only one runs at a time
bool midi_scan_active = false;
// set when a scan is requested while one is already running, so the worker
// rescans once instead of dropping the request (events that arrive during
// the slow initial scan would otherwise be lost)
bool midi_scan_pending = false;
// MIDI handles pending close. devices_destruct() runs under devices_mutex but
// midiInReset/midiInClose must not (they block on the MIDI callback, which
// also takes devices_mutex), so handles are queued here and closed by
// midi_close_deferred_flush() once the lock is released
std::vector<HMIDIIN> midi_close_deferred;
std::thread *flush_thread = nullptr;
bool flush_thread_running = false;
std::mutex flush_thread_m;
@@ -83,6 +117,8 @@ namespace rawinput {
void input_hwnd_create();
void input_hwnd_destroy();
void devices_reload();
void midi_scan_join();
void midi_close_deferred_flush();
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
void devices_scan_piuio();
void devices_scan_smxstage();
@@ -93,6 +129,7 @@ namespace rawinput {
void flush_stop();
void output_start();
void output_stop();
void devices_write_output_snapshot(bool only_updated);
static std::string rawinput_get_device_name(HANDLE hDevice);
static std::string rawinput_get_device_description(const DeviceInfo& info, const std::string &device_name);
@@ -101,6 +138,10 @@ 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);
// 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:
HWND input_hwnd = nullptr;
@@ -118,6 +159,7 @@ namespace rawinput {
void devices_scan_rawinput(const std::string &device_name = "");
void devices_scan_midi();
void midi_scan_start();
void devices_scan_xinput();
void devices_remove(const std::string &name);
@@ -130,11 +172,12 @@ namespace rawinput {
void __stdcall devices_print();
Device *devices_get(const std::string &name, bool updated = false);
inline std::vector<Device> &devices_get() {
inline std::list<Device> &devices_get() {
return devices;
}
inline std::vector<Device *> devices_get_updated() {
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
std::vector<Device *> updated;
for (auto &device : devices_get()) {
device.mutex->lock();
@@ -150,8 +193,10 @@ namespace rawinput {
}
inline void devices_midi_freeze(bool freeze) {
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;
};
}
+172 -22
View File
@@ -24,13 +24,118 @@ namespace rawinput::touch {
// settings
bool DISABLED = false;
bool INVERTED = false;
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
bool ASPECT_COMPENSATION_GAME = false;
// state
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
long DISPLAY_SIZE_X = 1920L;
long DISPLAY_SIZE_Y = 1080L;
long DISPLAY_NATIVE_X = 0L;
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:
return true;
case AspectMode::Off:
return false;
default:
return ASPECT_COMPENSATION_GAME;
}
}
// compute the centered letterbox inset (fraction cropped from each side) that results
// from showing the current display mode on a panel with a different native aspect
// ratio. only one of inset_x / inset_y is ever non-zero.
static void compute_letterbox_inset(double &inset_x, double &inset_y) {
inset_x = 0.0;
inset_y = 0.0;
// native resolution in the current display orientation
long native_x = DISPLAY_NATIVE_X;
long native_y = DISPLAY_NATIVE_Y;
if ((DISPLAY_SIZE_Y >= DISPLAY_SIZE_X) != (native_y >= native_x)) {
long tmp = native_x;
native_x = native_y;
native_y = tmp;
}
// compare the displayed aspect ratio against the panel's native one; ignore
// tiny mismatches (e.g. 1366x768 vs true 16:9) that would only add a sub-pixel
// inset, so near-matched panels stay an exact no-op
double current_aspect = (double) DISPLAY_SIZE_X / (double) DISPLAY_SIZE_Y;
double native_aspect = (double) native_x / (double) native_y;
constexpr double aspect_epsilon = 0.01;
if (current_aspect < native_aspect - aspect_epsilon) {
// image is narrower than the panel -> bars on left/right
inset_x = (1.0 - current_aspect / native_aspect) / 2.0;
} else if (current_aspect > native_aspect + aspect_epsilon) {
// image is wider than the panel -> bars on top/bottom
inset_y = (1.0 - native_aspect / current_aspect) / 2.0;
}
}
// undo letterboxing: remap the normalized (0.0 - 1.0) digitizer coordinates, which span
// the whole physical panel, into the displayed image region. returns false when the
// touch lands in the black bars (outside the image) and should be ignored.
static bool remap_aspect_compensation(double &norm_x, double &norm_y) {
if (DISPLAY_NATIVE_X <= 0 || DISPLAY_NATIVE_Y <= 0) {
return true;
}
double inset_x, inset_y;
compute_letterbox_inset(inset_x, inset_y);
// log once the first time the compensation actually engages (non-zero inset)
static bool logged_active = false;
if (!logged_active && (inset_x > 0.0 || inset_y > 0.0)) {
logged_active = true;
log_info("rawinput", "aspect compensation active: display {}x{}, native {}x{}, inset x={:.4f} y={:.4f}",
(int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y,
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y,
inset_x, inset_y);
}
// ignore touches inside the black bars (outside the displayed image)
if ((inset_x > 0.0 && (norm_x < inset_x || norm_x > 1.0 - inset_x)) ||
(inset_y > 0.0 && (norm_y < inset_y || norm_y > 1.0 - inset_y))) {
return false;
}
// remap the displayed image region back to the full 0.0 - 1.0 range
if (inset_x > 0.0) {
norm_x = (norm_x - inset_x) / (1.0 - 2.0 * inset_x);
}
if (inset_y > 0.0) {
norm_y = (norm_y - inset_y) / (1.0 - 2.0 * inset_y);
}
return true;
}
// determine the native (maximum) resolution advertised by the primary display device.
// used to detect and undo letterboxing when a non-native display mode is in use.
// falls back to the current display size if enumeration yields nothing larger.
static void detect_native_resolution() {
DISPLAY_NATIVE_X = DISPLAY_SIZE_X;
DISPLAY_NATIVE_Y = DISPLAY_SIZE_Y;
DEVMODE native_mode{};
native_mode.dmSize = sizeof(native_mode);
for (int i = 0; EnumDisplaySettings(nullptr, i, &native_mode); i++) {
if ((long) native_mode.dmPelsWidth * (long) native_mode.dmPelsHeight >
DISPLAY_NATIVE_X * DISPLAY_NATIVE_Y) {
DISPLAY_NATIVE_X = (long) native_mode.dmPelsWidth;
DISPLAY_NATIVE_Y = (long) native_mode.dmPelsHeight;
}
}
}
bool is_touchscreen(Device *device) {
// check if disabled
@@ -176,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
@@ -322,6 +427,18 @@ namespace rawinput::touch {
std::vector<DWORD> touch_removes;
std::vector<TouchPoint> touch_writes;
std::vector<DWORD> touch_modifications;
// drop every touch point with the given id (marking it as released)
auto remove_touch_point = [&] (DWORD id) {
touch_removes.push_back(id);
touch.touch_points.erase(std::remove_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[id] (const HIDTouchPoint &x) {
return x.id == id;
}), touch.touch_points.end());
};
for (auto &hid_tp : touch_points) {
// check if existing
@@ -353,23 +470,29 @@ namespace rawinput::touch {
// only remove if it exists
if (existing != touch.touch_points.end()) {
// remove all touch points with this ID
touch_removes.push_back(hid_tp.id);
touch.touch_points.erase(std::remove_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[hid_tp] (const HIDTouchPoint &x) {
return x.id == hid_tp.id;
}), touch.touch_points.end());
remove_touch_point(hid_tp.id);
}
} else {
// write touch point
// start from the normalized (0.0 - 1.0) digitizer coordinates
double norm_x = hid_tp.x;
double norm_y = hid_tp.y;
// undo letterboxing so raw input touches line up with the displayed
// image; touches that land in the black bars are treated as released
if (aspect_compensation_enabled() && !remap_aspect_compensation(norm_x, norm_y)) {
if (existing != touch.touch_points.end()) {
remove_touch_point(hid_tp.id);
}
continue;
}
// scale the normalized coordinates to the monitor to obtain the
// physical screen position of the touch
TouchPoint tp {
.id = hid_tp.id,
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
.x = (long) (norm_x * DISPLAY_SIZE_X),
.y = (long) (norm_y * DISPLAY_SIZE_Y),
.mouse = false,
};
touch_writes.push_back(tp);
@@ -390,13 +513,28 @@ namespace rawinput::touch {
}
// set TTL and last report time
if (!touch.elements_x.empty() && !touch_modifications.empty()) {
if (!touch.elements_x.empty()) {
// a HID digitizer reports only a fixed number of contacts per report and
// cycles the remaining ones through following reports, so a contact can be
// absent for up to this many reports before it comes back around
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
auto system_time_ms = get_system_milliseconds();
// extend every tracked contact to the current cycle length without ever
// shrinking it; otherwise a rising contact count (e.g. rapid taps) lengthens
// the cycle and can starve a stationary held contact into being reaped before
// it is reported again. the 50ms last-report timeout remains the hard backstop
for (auto &hid_tp : touch.touch_points) {
if (hid_tp.ttl < ttl) {
hid_tp.ttl = ttl;
}
}
// only contacts actually seen this report refresh their last-report time
for (auto &hid_tp_id : touch_modifications) {
for (auto &hid_tp : touch.touch_points) {
if (hid_tp.id == hid_tp_id) {
hid_tp.ttl = ttl;
hid_tp.last_report = system_time_ms;
}
}
@@ -502,18 +640,13 @@ 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);
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
log_info("rawinput", "primary display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
// determine monitor orientation
DEVMODE display_mode{};
@@ -561,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);
}
}
}
+19
View File
@@ -9,6 +9,25 @@ namespace rawinput::touch {
extern bool DISABLED;
extern bool INVERTED;
// aspect-ratio compensation: when the current display mode has a different aspect
// ratio than the panel's native resolution, the image is letterboxed/pillarboxed
// on the physical panel while the digitizer still spans the whole panel. this
// remaps the touch into the displayed image region.
enum class AspectMode {
Auto, // decided per game via ASPECT_COMPENSATION_GAME
On, // always compensate
Off, // never compensate
};
// global override, driven by the -rawtouchaspect launcher option
extern AspectMode ASPECT_COMPENSATION_MODE;
// per-game request, honored only when ASPECT_COMPENSATION_MODE is Auto
extern bool ASPECT_COMPENSATION_GAME;
// effective decision combining the global mode and the per-game request
bool aspect_compensation_enabled();
// global state
extern DWORD DISPLAY_ORIENTATION;
extern long DISPLAY_SIZE_X;
+18 -2
View File
@@ -179,11 +179,27 @@ XInputSetState(
XInputManager::XInputManager() {
log_info("xinput", "initialize...");
this->xinput_lib = LoadLibraryA("xinput1_3.dll");
// many systems ship only a newer XInput runtime, so try the known
// DLL names in order of preference until one loads
static const char *xinput_dll_names[] = {
"xinput1_4.dll",
"xinput1_3.dll",
"xinput9_1_0.dll",
};
const char *loaded_name = nullptr;
for (const char *name : xinput_dll_names) {
this->xinput_lib = LoadLibraryA(name);
if (this->xinput_lib) {
loaded_name = name;
break;
}
}
if (!this->xinput_lib) {
log_warning("xinput", "failed to load xinput1_3.dll");
log_warning("xinput", "failed to load any XInput DLL");
return;
}
log_info("xinput", "loaded {}", loaded_name);
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
GetProcAddress(this->xinput_lib, "XInputGetState"));
if (!XInputGetState_addr) {

Some files were not shown because too many files have changed in this diff Show More