Compare commits

..

62 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
bicarus cf86fbd238 graphics: remove static import of d3d9 (#780)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #779
Regressed by #720 

## Description of change
#720 introduced a static dependency on DX9 which caused `d3d9.dll` to be
loaded at boot from `system32`. Some third party hooks (like
ifs_layeredfs and dxvk) rely on supplying a custom `d3d9.dll` in the DLL
search path (usually in modules) but this change caused Windows to skip
that check.

Remove the hard dependency on d3d9 and add a CMake check to ensure that
compiled binaries do not accidentally introduce new static imports in
the future.

## Testing
Confirmed that dxvk runs again.
2026-06-27 15:23:59 -07:00
bicarus 3fdb369128 sdvx: option to disable Live2D (#777)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an option to disable Live2D. Can be disabled everywhere, or just
during a song.

## Testing
Tested EG final and recent Nabla.
2026-06-27 02:34:19 -07:00
bicarus db7defff5a overlay: fix OS cursor showing up in some games (#776)
## Link to GitHub Issue or related Pull Request, if one exists
regressed by #766, fixes #775 

## Description of change
Some games like DDR leave `ShowCursor` ref count at non-negative number,
so when spice handles `WM_SETCURSOR` to change the cursor shape, it ends
up showing the OS cursor.

## Testing
2026-06-25 15:30:59 -07:00
bicarus 451cbec0b9 overlay: OBS control window via WebSocket API (#773)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an overlay window widget for controlling OBS over WebSocket.

Add new lines to the FPS widget that shows recording / streaming timers.

Show notifications for major state changes (stream started/stopped,
recording started/paused/resumed/stopped)

## Testing
2026-06-24 02:44:36 -07:00
bicarus 5c69e295ab ccj: fix mouse trackball cursor wrap (#774)
I don't have a repro but a user reported that the cursor doesn't wrap
around on one side. Correctly account for windows bleeding over to other
monitors.
2026-06-21 17:03:44 -07:00
bicarus 5065a92d55 launcher: don't warn about hook option conflicts (#771)
DLL hook options (-k and -z) allow multiple flags being specified in
both spicecfg and via command line; they never conflict as they are
additive. Remove the warning about option conflicts.
2026-06-20 15:46:45 -07:00
bicarus 5483e8e2c0 ponp: make Force Sub Redraw an option (#770)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to how it is on SDVX, turning this on can cause graphical
glitches depending on the GPU, so it needs to be an option that users
can toggle.

## Testing
2026-06-19 22:30:08 -07:00
bicarus e8949a2612 overlay: fix highlights, alignment in presets table (#769)
* Fix table row highlight hover detection in Options tab when row has
two lines of text
* Fix text alignment in controller presets table
2026-06-19 00:40:54 -07:00
bicarus db71a0b24d cfg: center window on launch (#768) 2026-06-18 23:02:23 -07:00
bicarus d3d5422768 cfg: enforce minimum window size (#767) 2026-06-18 21:10:42 -07:00
bicarus-dev 5185f753e5 fix truncated text 2026-06-18 17:32:59 -07:00
143 changed files with 13137 additions and 5296 deletions
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
run: run:
working-directory: ./src/spice2x working-directory: ./src/spice2x
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set ccache environment variables - name: Set ccache environment variables
run: | run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache - name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2 uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA - name: Calculate commit SHA
id: vars id: vars
run: | run: |
@@ -25,7 +25,7 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile - name: Compile
run: ./build_docker.sh run: ./build_docker.sh
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v6
with: with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }} name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin 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 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. * 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 * Performing decryption of encrypted files.
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data * 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) * Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
* This includes automatic updaters. * 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. * 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 ### Avoiding regressions
+69 -2
View File
@@ -408,13 +408,16 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/iidx/mf_wrappers.cpp games/iidx/mf_wrappers.cpp
games/sdvx/bi2x_hook.cpp games/sdvx/bi2x_hook.cpp
games/sdvx/sdvx.cpp games/sdvx/sdvx.cpp
games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp games/sdvx/io.cpp
games/sdvx/camera.cpp games/sdvx/camera.cpp
games/jb/jb.cpp games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp games/jb/io.cpp
games/nost/nost.cpp games/nost/nost.cpp
games/nost/io.cpp games/nost/io.cpp
games/nost/poke.cpp games/nost/poke.cpp
games/nost/touch_mode.cpp
games/gitadora/gitadora.cpp games/gitadora/gitadora.cpp
games/gitadora/asio.cpp games/gitadora/asio.cpp
games/gitadora/io.cpp games/gitadora/io.cpp
@@ -430,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/rb/rb.cpp games/rb/rb.cpp
games/rb/io.cpp games/rb/io.cpp
games/rb/touch.cpp games/rb/touch.cpp
games/rb/touch_debug.cpp
games/bs/bs.cpp games/bs/bs.cpp
games/bs/io.cpp games/bs/io.cpp
games/rf3d/rf3d.cpp games/rf3d/rf3d.cpp
@@ -499,6 +503,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/audio/buffer.cpp hooks/audio/buffer.cpp
hooks/audio/mme.cpp hooks/audio/mme.cpp
hooks/audio/util.cpp hooks/audio/util.cpp
hooks/audio/xact.cpp
hooks/audio/backends/dsound/dsound_backend.cpp hooks/audio/backends/dsound/dsound_backend.cpp
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
hooks/audio/backends/mmdevice/device.cpp hooks/audio/backends/mmdevice/device.cpp
@@ -525,10 +530,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp hooks/devicehook.cpp
hooks/graphics/graphics.cpp hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp hooks/graphics/graphics_windowed.cpp
hooks/graphics/nvapi_impl.cpp
hooks/graphics/nvapi_hook.cpp hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_texture.cpp hooks/graphics/backends/d3d9/d3d9_texture.cpp
@@ -567,7 +574,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp misc/device.cpp
misc/eamuse.cpp misc/eamuse.cpp
misc/extdev.cpp misc/extdev.cpp
misc/nativetouchhook.cpp
misc/sciunit.cpp misc/sciunit.cpp
misc/sde.cpp misc/sde.cpp
misc/wintouchemu.cpp misc/wintouchemu.cpp
@@ -605,12 +611,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp overlay/windows/keypad.cpp
overlay/windows/log.cpp overlay/windows/log.cpp
overlay/windows/midi.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/patch_manager.cpp
overlay/windows/popn_sub.cpp overlay/windows/popn_sub.cpp
overlay/windows/wnd_manager.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/rawinput.cpp rawinput/rawinput.cpp
rawinput/rawinput_handles.cpp
rawinput/midi.cpp
rawinput/sextet.cpp rawinput/sextet.cpp
rawinput/piuio.cpp rawinput/piuio.cpp
rawinput/touch.cpp rawinput/touch.cpp
@@ -637,8 +658,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
stubs/stubs.cpp stubs/stubs.cpp
# touch # 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.cpp
touch/touch_indicators.cpp touch/touch_gestures.cpp
touch/win7.cpp touch/win7.cpp
touch/win8.cpp touch/win8.cpp
@@ -671,6 +698,33 @@ set(SOURCE_FILES ${SOURCE_FILES}
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
# guard against statically importing DLLs that must always be loaded dynamically:
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
# static import loads the system copy at startup and preempts the override
# (issue #779).
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
# Unity games.
# the check runs objdump on each produced binary and fails the build if any
# forbidden DLL is imported.
set(SPICE_FORBIDDEN_STATIC_IMPORTS
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
mf.dll mfplat.dll mfreadwrite.dll)
function(spice_guard_dll_imports target)
if(MSVC)
# objdump-based parsing assumes a GNU/LLVM toolchain
return()
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DOBJDUMP=${CMAKE_OBJDUMP}
-DTARGET_FILE=$<TARGET_FILE:${target}>
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
VERBATIM
COMMENT "Checking ${target} for forbidden static DLL imports")
endfunction()
# spice.exe / spice_laa.exe shared objects # spice.exe / spice_laa.exe shared objects
########################################### ###########################################
# spice.exe and spice_laa.exe are compiled identically; the only difference is # spice.exe and spice_laa.exe are compiled identically; the only difference is
@@ -955,3 +1009,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
# forbidden static DLL import guard
###################################
# apply the check to every executable and DLL produced by this project.
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
foreach(spice_target IN LISTS spice_all_targets)
get_target_property(spice_target_type ${spice_target} TYPE)
if(spice_target_type STREQUAL "EXECUTABLE"
OR spice_target_type STREQUAL "SHARED_LIBRARY"
OR spice_target_type STREQUAL "MODULE_LIBRARY")
spice_guard_dll_imports(${spice_target})
endif()
endforeach()
+3 -1
View File
@@ -4,6 +4,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "touch/native/nativetouchhook.h"
// static stuff // static stuff
static int ACIO_WARMUP = 0; static int ACIO_WARMUP = 0;
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
// flush device output // flush device output
RI_MGR->devices_flush_output(); RI_MGR->devices_flush_output();
// update wintouchemu // update touch emulation
wintouchemu::update(); wintouchemu::update();
nativetouch::refresh_contact_lifetime();
return 1; return 1;
} }
+12
View File
@@ -3,6 +3,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/nost/io.h" #include "games/nost/io.h"
#include "games/nost/nost.h" #include "games/nost/nost.h"
#include "games/nost/touch_mode.h"
#include "util/logging.h" #include "util/logging.h"
#include "avs/game.h" #include "avs/game.h"
@@ -16,6 +17,7 @@ using namespace GameAPI;
// static stuff // static stuff
static uint8_t STATUS_BUFFER[277]; static uint8_t STATUS_BUFFER[277];
static bool STATUS_BUFFER_FREEZE = false; static bool STATUS_BUFFER_FREEZE = false;
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
/* /*
* Implementations * Implementations
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
games::nost::Analogs::Key27, games::nost::Analogs::Key28, games::nost::Analogs::Key27, games::nost::Analogs::Key28,
}; };
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
// iterate pairs of keys // iterate pairs of keys
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) { 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) { if (button0 > 0) {
state0 = button0; 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( const auto button1 = panb_get_button_velocity(
buttons.at(button_mapping[key_pair * 2 + 1]), 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) { if (button1 > 0) {
state1 = button1; 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 // build value
uint8_t value = 0; uint8_t value = 0;
+84 -2
View File
@@ -1,5 +1,7 @@
#include "touch.h" #include "touch.h"
#include <algorithm>
#include <cstdint>
#include <functional> #include <functional>
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
@@ -8,6 +10,8 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/utils.h" #include "util/utils.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
@@ -17,6 +21,25 @@ using namespace rapidjson;
namespace api::modules { 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") { Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC"); is_sdvx = avs::game::is_model("KFC");
@@ -26,6 +49,25 @@ namespace api::modules {
is_tdj_fhd = false; 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["read"] = std::bind(&Touch::read, this, _1, _2);
functions["write"] = std::bind(&Touch::write, this, _1, _2); functions["write"] = std::bind(&Touch::write, this, _1, _2);
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2); functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
@@ -99,7 +141,11 @@ namespace api::modules {
} }
// apply touch points // 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 // 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) { void Touch::apply_touch_errata(int &x, int &y) {
+14
View File
@@ -1,8 +1,12 @@
#pragma once #pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <vector> #include <vector>
#include "api/module.h" #include "api/module.h"
#include "api/request.h" #include "api/request.h"
#include "touch/touch.h"
namespace api::modules { namespace api::modules {
@@ -13,11 +17,21 @@ namespace api::modules {
private: private:
bool is_sdvx; bool is_sdvx;
bool is_tdj_fhd; 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 // function definitions
void read(Request &req, Response &res); void read(Request &req, Response &res);
void write(Request &req, Response &res); void write(Request &req, Response &res);
void write_reset(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); void apply_touch_errata(int &x, int &y);
}; };
+18 -1
View File
@@ -214,6 +214,19 @@ time (
echo "===========================" 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)) if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then then
echo "" echo ""
@@ -229,7 +242,7 @@ then
fi fi
if ((BUILD_XP_32 > 0)) if ((BUILD_XP_32 > 0))
then 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/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe ${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi fi
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32 mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64 mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0)) if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then then
mkdir -p ${OUTDIR_EXTRAS}/winxp mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} + find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api 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 # build distribution archive
if ((DIST_ENABLE > 0)) if ((DIST_ENABLE > 0))
then 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 <cassert>
#include <optional> #include <optional>
#include "games/io.h"
#include "launcher/superexit.h" #include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "rawinput/piuio.h" #include "rawinput/piuio.h"
#include "util/time.h" #include "util/time.h"
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
return sorted; 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 // check override
if (_button.override_enabled) { if (_button.override_enabled) {
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
std::optional<bool> window_has_focus; std::optional<bool> window_has_focus;
while (true) { 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 // naive behavior
if (current_button->isNaive()) { if (current_button->isNaive()) {
GameAPI::Buttons::State state; GameAPI::Buttons::State state;
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
BUTTON_PRESSED : BUTTON_NOT_PRESSED; BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break; 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: default:
break; 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) { Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
if (manager) { if (manager) {
return getState(manager.get(), button, check_alts); return getState(manager.get(), button, check_alts);
@@ -478,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
return button.override_velocity; 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 // 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 // check if button isn't being pressed
if (button_state != Buttons::BUTTON_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)); 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 // get VKey string
auto vKey = (uint16_t) this->getVKey(); auto vKey = (uint16_t) this->getVKey();
+11
View File
@@ -49,6 +49,7 @@ private:
std::string name; std::string name;
std::string device_identifier = ""; std::string device_identifier = "";
unsigned short vKey = INVALID_VKEY; unsigned short vKey = INVALID_VKEY;
uint8_t modifier_mask = 0;
// default bindings are always naive // default bindings are always naive
unsigned short vKey_default = INVALID_VKEY; unsigned short vKey_default = INVALID_VKEY;
@@ -65,6 +66,7 @@ private:
unsigned short velocity_threshold = 0; unsigned short velocity_threshold = 0;
std::string getMidiNoteString(); std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public: public:
std::string getVKeyString(); std::string getVKeyString();
@@ -111,6 +113,7 @@ public:
device_identifier = ""; device_identifier = "";
analog_type = BAT_NONE; analog_type = BAT_NONE;
bat_threshold = 0; bat_threshold = 0;
modifier_mask = 0;
} }
std::string getDisplayString(rawinput::RawInputManager* manager); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -123,6 +126,14 @@ public:
return this->name; 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 { inline const std::string &getDeviceIdentifier() const {
return this->device_identifier; return this->device_identifier;
} }
+12
View File
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
int bat_threshold = 0; int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
unsigned int modifier_mask = 0;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey); tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
const char *devid = gameButtonNode->Attribute("devid"); const char *devid = gameButtonNode->Attribute("devid");
gameButtonNode->QueryIntAttribute("analogtype", &analogType); gameButtonNode->QueryIntAttribute("analogtype", &analogType);
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold); gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold); gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert); gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) { if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
gameButtonsNode->DeleteChild(gameButtonNode); gameButtonsNode->DeleteChild(gameButtonNode);
gameButtonNode = this->configFile.NewElement("button"); gameButtonNode = this->configFile.NewElement("button");
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", bat_threshold); gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold); gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert); gameButtonNode->SetAttribute("invert", invert);
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
gameButtonsNode->InsertEndChild(gameButtonNode); gameButtonsNode->InsertEndChild(gameButtonNode);
} else { } else {
button->setVKey(static_cast<unsigned short int>(vKey)); button->setVKey(static_cast<unsigned short int>(vKey));
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
button->setBatThreshold(bat_threshold); button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold); button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert); button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) { if (devid) {
button->setDeviceIdentifier(devid); button->setDeviceIdentifier(devid);
} }
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold()); gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold()); gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert()); gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode); gameButtonsNode->InsertEndChild(gameButtonNode);
} }
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold()); gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold()); gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert()); gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode); 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("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert()); gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break; break;
} }
} }
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", 0); gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false); gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", ""); gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode); gameButtonsNode->InsertEndChild(gameButtonNode);
} }
} }
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
int bat_threshold = 0; int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
unsigned int modifier_mask = 0;
gameButtonNode->QueryIntAttribute("vkey", &vKey); gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType); gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up); 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("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold); gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert); gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
const char *devid = gameButtonNode->Attribute("devid"); const char *devid = gameButtonNode->Attribute("devid");
// find alternative // find alternative
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setBatThreshold(bat_threshold); alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold); alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert); alt.setInvert(invert);
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) { if (devid) {
alt.setDeviceIdentifier(std::string(devid)); alt.setDeviceIdentifier(std::string(devid));
} }
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setBatThreshold(bat_threshold); button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold); button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert); button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) { if (devid) {
button.setDeviceIdentifier(devid); button.setDeviceIdentifier(devid);
} }
+21 -1
View File
@@ -3,6 +3,7 @@
#include <d3d9.h> #include <d3d9.h>
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
namespace cfg { namespace cfg {
@@ -35,7 +36,26 @@ namespace cfg {
return false; return false;
} }
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION); // load d3d9.dll dynamically rather than linking against it statically.
// a static import would force the system d3d9.dll to load at process
// startup for the main spice executable too (this file is shared with
// spice.exe), which loads system32\d3d9.dll before the modules directory
// is added to the DLL search path - preventing a user-supplied DXVK
// d3d9.dll in modules from ever loading for the game.
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
if (d3d9_module == nullptr) {
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
return false;
}
auto Direct3DCreate9_fn =
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
if (Direct3DCreate9_fn == nullptr) {
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
return false;
}
IDirect3D9 *d3d = Direct3DCreate9_fn(D3D_SDK_VERSION);
if (d3d == nullptr) { if (d3d == nullptr) {
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer"); log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
return false; return false;
+38 -1
View File
@@ -21,6 +21,8 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
static std::string WINDOW_TITLE; static std::string WINDOW_TITLE;
static int WINDOW_SIZE_X = 800; static int WINDOW_SIZE_X = 800;
static int WINDOW_SIZE_Y = 600; static int WINDOW_SIZE_Y = 600;
static const int WINDOW_MIN_SIZE_X = 540;
static const int WINDOW_MIN_SIZE_Y = 300;
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
static const UINT_PTR RENDER_TIMER_ID = 1; static const UINT_PTR RENDER_TIMER_ID = 1;
@@ -85,6 +87,23 @@ static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
GetWindowLongPtrW(hWnd, GWLP_USERDATA)); GetWindowLongPtrW(hWnd, GWLP_USERDATA));
} }
// computes the top-left position that centers a window of the given size on
// the primary monitor's work area (the desktop minus the taskbar); falls back
// to (0, 0) if the monitor info can't be queried.
static POINT center_on_primary_monitor(int width, int height) {
POINT pos = { 0, 0 };
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(mon, &mi)) {
const int work_w = mi.rcWork.right - mi.rcWork.left;
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
pos.x = mi.rcWork.left + (work_w - width) / 2;
pos.y = mi.rcWork.top + (work_h - height) / 2;
}
return pos;
}
// Returns the refresh rate (Hz) of the monitor the window currently lives on, // Returns the refresh rate (Hz) of the monitor the window currently lives on,
// clamped to a sane range. Falls back to 60 if detection fails or the value // clamped to a sane range. Falls back to 60 if detection fails or the value
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default"). // looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
@@ -243,8 +262,10 @@ cfg::ConfiguratorWindow::~ConfiguratorWindow() {
void cfg::ConfiguratorWindow::run() { void cfg::ConfiguratorWindow::run() {
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
// show window // show window
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
ShowWindow(this->hWnd, SW_SHOWNORMAL); ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd); UpdateWindow(this->hWnd);
@@ -290,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
} }
break; break;
} }
case WM_GETMINMAXINFO: {
// enforce a minimum window size so the UI can't be shrunk to a
// point where the tabs/controls become unusable. The minimum is
// expressed in client-area pixels and converted to a full window
// size that accounts for the current frame/title-bar style.
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
}
break;
}
case WM_CREATE: { case WM_CREATE: {
// set user data of window to class pointer // set user data of window to class pointer
+46
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
el->SetAttribute("debounce_down", entry.debounce_down); el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold); el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold); el->SetAttribute("velocity_threshold", entry.velocity_threshold);
el->SetAttribute("modifiers", entry.modifier_mask);
parent->InsertEndChild(el); parent->InsertEndChild(el);
} }
@@ -54,6 +55,10 @@ namespace overlay::windows {
el->QueryIntAttribute("bat_threshold", &bat); el->QueryIntAttribute("bat_threshold", &bat);
entry.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; 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 // keypad buttons
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons"); auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
if (kp_el) { if (kp_el) {
@@ -244,6 +271,7 @@ namespace overlay::windows {
bool save_user_template( bool save_user_template(
const ControllerTemplate &tmpl, const ControllerTemplate &tmpl,
const bool save_buttons, const bool save_buttons,
const bool save_modifiers,
const bool save_keypads, const bool save_keypads,
const bool save_analogs, const bool save_analogs,
const bool save_lights) { const bool save_lights) {
@@ -300,6 +328,24 @@ namespace overlay::windows {
} }
tmpl_el->InsertEndChild(buttons_el); 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 // keypad buttons — skip unbound entries
if (!tmpl.keypad_buttons.empty()) { if (!tmpl.keypad_buttons.empty()) {
auto *kp_el = doc.NewElement("keypad_buttons"); auto *kp_el = doc.NewElement("keypad_buttons");
@@ -0,0 +1,72 @@
# fails the build if a PE binary statically imports a forbidden DLL.
#
# some DLLs must never end up in spice's static import table, for two reasons:
#
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
# into the modules directory to replace the system one. a static import
# forces the loader to load the SYSTEM copy at process startup - before the
# modules directory is added to the DLL search path and before the game DLL
# loads - so the user-supplied override never takes effect (see issue #779).
#
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
#
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
#
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
# TARGET_FILE - path to the PE binary to inspect
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
message(WARNING
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
return()
endif()
execute_process(
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
OUTPUT_VARIABLE dump_output
RESULT_VARIABLE dump_result
ERROR_VARIABLE dump_error)
if(NOT dump_result EQUAL 0)
message(WARNING
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
return()
endif()
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
# statically imported DLL in their PE private-header dump.
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
set(violations "")
foreach(line IN LISTS dll_lines)
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
string(STRIP "${dll_name}" dll_name)
string(TOLOWER "${dll_name}" dll_name_lower)
if(dll_name_lower IN_LIST FORBIDDEN)
list(APPEND violations "${dll_name}")
endif()
endforeach()
if(violations)
list(REMOVE_DUPLICATES violations)
string(REPLACE ";" ", " violations_str "${violations}")
message(FATAL_ERROR
"static DLL import check FAILED for ${TARGET_FILE}\n"
" forbidden static imports found: ${violations_str}\n"
"\n"
" these DLLs must never be statically imported by spice:\n"
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
" system copy at startup and preempts the modules override (issue #779).\n"
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
" Unity games.\n"
"\n"
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
" call through the resolved function pointer.")
endif()
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
+552
View File
@@ -0,0 +1,552 @@
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include "easywsclient.hpp"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment( lib, "ws2_32" )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <string>
typedef SOCKET socket_t;
#ifndef _SSIZE_T_DEFINED
typedef int ssize_t;
#define _SSIZE_T_DEFINED
#endif
#ifndef _SOCKET_T_DEFINED
typedef SOCKET socket_t;
#define _SOCKET_T_DEFINED
#endif
#if defined(_MSC_VER) && !defined(snprintf)
#define snprintf _snprintf_s
#endif
#include <stdint.h>
#define socketerrno WSAGetLastError()
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
#else
#include <fcntl.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <errno.h>
typedef int socket_t;
#ifndef INVALID_SOCKET
#define INVALID_SOCKET (-1)
#endif
#ifndef SOCKET_ERROR
#define SOCKET_ERROR (-1)
#endif
#define closesocket(s) ::close(s)
#include <errno.h>
#define socketerrno errno
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
#endif
#include <vector>
#include <string>
#include <stdarg.h>
#include "util/logging.h"
// When false, easywsclient's diagnostics are suppressed. The host application
// opts in by setting this flag (see obs_websocket.cpp).
bool EASYWSCLIENT_LOGGING_ENABLED = false;
// Route easywsclient's diagnostic output through the project logger instead of
// writing to stderr, without editing the upstream source lines below: these two
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
static inline void easywsclient_logf(const char *fmt, ...) {
if (!EASYWSCLIENT_LOGGING_ENABLED) {
return;
}
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// trim trailing newline(s); the logger appends its own
size_t len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
log_misc("easywsclient", "{}", buf);
}
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
#define fputs(str, stream) easywsclient_logf("%s", str)
using namespace easywsclient;
namespace { // private module-only namespace
socket_t hostname_connect(const std::string& hostname, int port) {
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *p;
int ret;
socket_t sockfd = INVALID_SOCKET;
char sport[16];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(sport, 16, "%d", port);
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
for(p = result; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == INVALID_SOCKET) { continue; }
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
break;
}
closesocket(sockfd);
sockfd = INVALID_SOCKET;
}
freeaddrinfo(result);
return sockfd;
}
class _DummyWebSocket : public easywsclient::WebSocket
{
public:
void poll(int timeout) { }
void send(const std::string& message) { }
void sendBinary(const std::string& message) { }
void sendBinary(const std::vector<uint8_t>& message) { }
void sendPing() { }
void close() { }
readyStateValues getReadyState() const { return CLOSED; }
void _dispatch(Callback_Imp & callable) { }
void _dispatchBinary(BytesCallback_Imp& callable) { }
};
class _RealWebSocket : public easywsclient::WebSocket
{
public:
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
struct wsheader_type {
unsigned header_size;
bool fin;
bool mask;
enum opcode_type {
CONTINUATION = 0x0,
TEXT_FRAME = 0x1,
BINARY_FRAME = 0x2,
CLOSE = 8,
PING = 9,
PONG = 0xa,
} opcode;
int N0;
uint64_t N;
uint8_t masking_key[4];
};
std::vector<uint8_t> rxbuf;
std::vector<uint8_t> txbuf;
std::vector<uint8_t> receivedData;
socket_t sockfd;
readyStateValues readyState;
bool useMask;
bool isRxBad;
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
}
readyStateValues getReadyState() const {
return readyState;
}
void poll(int timeout) { // timeout in milliseconds
if (readyState == CLOSED) {
if (timeout > 0) {
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
select(0, NULL, NULL, NULL, &tv);
}
return;
}
if (timeout != 0) {
fd_set rfds;
fd_set wfds;
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sockfd, &rfds);
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
}
while (true) {
// FD_ISSET(0, &rfds) will be true
int N = rxbuf.size();
ssize_t ret;
rxbuf.resize(N + 1500);
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
if (false) { }
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
rxbuf.resize(N);
break;
}
else if (ret <= 0) {
rxbuf.resize(N);
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
rxbuf.resize(N + ret);
}
}
while (txbuf.size()) {
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
if (false) { } // ??
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
break;
}
else if (ret <= 0) {
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
}
}
if (!txbuf.size() && readyState == CLOSING) {
closesocket(sockfd);
readyState = CLOSED;
}
}
virtual void _dispatch(Callback_Imp & callable) {
struct CallbackAdapter : public BytesCallback_Imp
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
{
Callback_Imp& callable;
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) {
std::string stringMessage(message.begin(), message.end());
callable(stringMessage);
}
};
CallbackAdapter bytesCallback(callable);
_dispatchBinary(bytesCallback);
}
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
// TODO: consider acquiring a lock on rxbuf...
while (true) {
wsheader_type ws;
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
ws.fin = (data[0] & 0x80) == 0x80;
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
ws.mask = (data[1] & 0x80) == 0x80;
ws.N0 = (data[1] & 0x7f);
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
int i = 0;
if (ws.N0 < 126) {
ws.N = ws.N0;
i = 2;
}
else if (ws.N0 == 126) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 8;
ws.N |= ((uint64_t) data[3]) << 0;
i = 4;
}
else if (ws.N0 == 127) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 56;
ws.N |= ((uint64_t) data[3]) << 48;
ws.N |= ((uint64_t) data[4]) << 40;
ws.N |= ((uint64_t) data[5]) << 32;
ws.N |= ((uint64_t) data[6]) << 24;
ws.N |= ((uint64_t) data[7]) << 16;
ws.N |= ((uint64_t) data[8]) << 8;
ws.N |= ((uint64_t) data[9]) << 0;
i = 10;
}
if (ws.mask) {
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
}
else {
ws.masking_key[0] = 0;
ws.masking_key[1] = 0;
ws.masking_key[2] = 0;
ws.masking_key[3] = 0;
}
// Note: The checks above should hopefully ensure this addition
// cannot overflow:
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
// We got a whole message, now do something with it:
if (false) { }
else if (
ws.opcode == wsheader_type::TEXT_FRAME
|| ws.opcode == wsheader_type::BINARY_FRAME
|| ws.opcode == wsheader_type::CONTINUATION
) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
if (ws.fin) {
callable((const std::vector<uint8_t>) receivedData);
receivedData.erase(receivedData.begin(), receivedData.end());
std::vector<uint8_t> ().swap(receivedData);// free memory
}
}
else if (ws.opcode == wsheader_type::PING) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
}
else if (ws.opcode == wsheader_type::PONG) { }
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
}
}
void sendPing() {
std::string empty;
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
}
void send(const std::string& message) {
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::string& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::vector<uint8_t>& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
template<class Iterator>
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
// TODO:
// Masking key should (must) be derived from a high quality random
// number generator, to mitigate attacks on non-WebSocket friendly
// middleware:
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
// TODO: consider acquiring a lock on txbuf...
if (readyState == CLOSING || readyState == CLOSED) { return; }
std::vector<uint8_t> header;
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
header[0] = 0x80 | type;
if (false) { }
else if (message_size < 126) {
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
if (useMask) {
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
}
}
else if (message_size < 65536) {
header[1] = 126 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 8) & 0xff;
header[3] = (message_size >> 0) & 0xff;
if (useMask) {
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
}
}
else { // TODO: run coverage testing here
header[1] = 127 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 56) & 0xff;
header[3] = (message_size >> 48) & 0xff;
header[4] = (message_size >> 40) & 0xff;
header[5] = (message_size >> 32) & 0xff;
header[6] = (message_size >> 24) & 0xff;
header[7] = (message_size >> 16) & 0xff;
header[8] = (message_size >> 8) & 0xff;
header[9] = (message_size >> 0) & 0xff;
if (useMask) {
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
}
}
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
txbuf.insert(txbuf.end(), header.begin(), header.end());
txbuf.insert(txbuf.end(), message_begin, message_end);
if (useMask) {
size_t message_offset = txbuf.size() - message_size;
for (size_t i = 0; i != message_size; ++i) {
txbuf[message_offset + i] ^= masking_key[i&0x3];
}
}
}
void close() {
if(readyState == CLOSING || readyState == CLOSED) { return; }
readyState = CLOSING;
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
std::vector<uint8_t> header(closeFrame, closeFrame+6);
txbuf.insert(txbuf.end(), header.begin(), header.end());
}
};
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
char host[512];
int port;
char path[512];
if (url.size() >= 512) {
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
return NULL;
}
if (origin.size() >= 200) {
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
return NULL;
}
if (false) { }
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
}
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
port = 80;
}
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
path[0] = '\0';
}
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
port = 80;
path[0] = '\0';
}
else {
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
return NULL;
}
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
socket_t sockfd = hostname_connect(host, port);
if (sockfd == INVALID_SOCKET) {
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
return NULL;
}
{
// XXX: this should be done non-blocking,
char line[1024];
int status;
int i;
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
if (port == 80) {
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
}
else {
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
if (!origin.empty()) {
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
line[i] = 0;
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
// TODO: verify response headers,
while (true) {
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
if (line[0] == '\r' && line[1] == '\n') { break; }
}
}
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
#ifdef _WIN32
u_long on = 1;
ioctlsocket(sockfd, FIONBIO, &on);
#else
fcntl(sockfd, F_SETFL, O_NONBLOCK);
#endif
//fprintf(stderr, "Connected to: %s\n", url.c_str());
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
}
} // end of module-only namespace
namespace easywsclient {
WebSocket::pointer WebSocket::create_dummy() {
static pointer dummy = pointer(new _DummyWebSocket);
return dummy;
}
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
return ::from_url(url, true, origin);
}
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
return ::from_url(url, false, origin);
}
} // namespace easywsclient
+73
View File
@@ -0,0 +1,73 @@
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include <string>
#include <vector>
#include <cstdint>
namespace easywsclient {
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
class WebSocket {
public:
typedef WebSocket * pointer;
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
// Factories:
static pointer create_dummy();
static pointer from_url(const std::string& url, const std::string& origin = std::string());
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
// Interfaces:
virtual ~WebSocket() { }
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
virtual void send(const std::string& message) = 0;
virtual void sendBinary(const std::string& message) = 0;
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
virtual void sendPing() = 0;
virtual void close() = 0;
virtual readyStateValues getReadyState() const = 0;
template<class Callable>
void dispatch(Callable callable)
// For callbacks that accept a string argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public Callback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::string& message) { callable(message); }
};
_Callback callback(callable);
_dispatch(callback);
}
template<class Callable>
void dispatchBinary(Callable callable)
// For callbacks that accept a std::vector<uint8_t> argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public BytesCallback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) { callable(message); }
};
_Callback callback(callable);
_dispatchBinary(callback);
}
protected:
virtual void _dispatch(Callback_Imp& callable) = 0;
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
};
} // namespace easywsclient
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
-1
View File
@@ -7,7 +7,6 @@
#include "util/utils.h" #include "util/utils.h"
#include "util/detour.h" #include "util/detour.h"
#include "acioemu/handle.h" #include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "trackball.h" #include "trackball.h"
+150 -98
View File
@@ -33,13 +33,14 @@ namespace games::ccj {
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices, static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
UINT cbSize) { UINT cbSize) {
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize); auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
if (result == 0xFFFFFFFF) if (result == 0xFFFFFFFF) {
return result; return result;
}
if (pRawInputDeviceList == NULL) { if (pRawInputDeviceList == NULL) {
(*puiNumDevices)++; (*puiNumDevices)++;
} else if (result < *puiNumDevices) { } else if (result < *puiNumDevices) {
pRawInputDeviceList[result] = {fakeHandle, 0}; pRawInputDeviceList[result] = { fakeHandle, 0 };
result++; result++;
} }
@@ -47,8 +48,9 @@ namespace games::ccj {
} }
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) { static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize); return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
}
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t); const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
@@ -68,23 +70,157 @@ namespace games::ccj {
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) { static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
wchar_t buffer[256]; wchar_t buffer[256];
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong); return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
}
hWnd = _hWnd; hWnd = _hWnd;
wndProc = (WNDPROC)dwNewLong; wndProc = (WNDPROC)dwNewLong;
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong); return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
} }
// compute the cursor wrap region in client coordinates. The cursor is confined to the
// monitor, so if the window's client area extends past a screen edge (window larger than /
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
// the left/top edges.
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
RECT bounds = client;
POINT origin = { 0, 0 };
ClientToScreen(wnd, &origin);
RECT clientScreen = {
origin.x,
origin.y,
origin.x + client.right,
origin.y + client.bottom
};
MONITORINFO mi = {};
mi.cbSize = sizeof(mi);
RECT usable;
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
bounds.left = usable.left - origin.x;
bounds.top = usable.top - origin.y;
bounds.right = usable.right - origin.x;
bounds.bottom = usable.bottom - origin.y;
}
return bounds;
}
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
static bool active = false;
static bool lastState = false;
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
static const std::chrono::milliseconds debounceDuration(100);
const auto currentTime = std::chrono::steady_clock::now();
const bool pressed = get_async_secondary_mouse();
const bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
const bool engaged = focused
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
if (!engaged) {
if (lastState && !active) {
lastState = false;
}
return;
}
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
const RECT bounds = trackball_wrap_bounds(hWnd, client);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
// can keep spinning past the screen edge.
bool updateCursor = false;
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
if (value <= lo) {
updateCursor = true;
return hi - 5;
}
if (value >= hi - 1) {
updateCursor = true;
return lo + 5;
}
return value;
};
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
}
// drive the trackball from the configured analog axes / direction buttons.
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
}
}
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) { static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
if (hRawInput != fakeHandle) if (hRawInput != fakeHandle) {
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader); return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
}
if (pData == NULL) { if (pData == NULL) {
if (uiCommand == RID_HEADER) if (uiCommand == RID_HEADER) {
*pcbSize = sizeof(RAWINPUTHEADER); *pcbSize = sizeof(RAWINPUTHEADER);
else } else {
*pcbSize = sizeof(RAWINPUT); *pcbSize = sizeof(RAWINPUT);
}
return 0; return 0;
} }
@@ -107,92 +243,9 @@ namespace games::ccj {
RAWMOUSE rawMouse {}; RAWMOUSE rawMouse {};
if (MOUSE_TRACKBALL) { if (MOUSE_TRACKBALL) {
static bool active = false; trackball_mouse_input(rawMouse);
static bool lastState = false;
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse();
bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
int sx = client.right - client.left;
int sy = client.bottom - client.top;
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
bool updateCursor = false;
if (cursor.x <= 0) {
updateCursor = true;
cursor.x = sx - 5;
} else if (cursor.x >= sx - 1) {
updateCursor = true;
cursor.x = 5;
}
if (cursor.y <= 0) {
updateCursor = true;
cursor.y = sy - 5;
} else if (cursor.y >= sy - 1) {
updateCursor = true;
cursor.y = 5;
}
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
} else if (lastState && !active) {
lastState = false;
}
} else { } else {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE; trackball_mapped_input(rawMouse);
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
} }
*((RAWINPUT*)pData) = { header, { rawMouse } }; *((RAWINPUT*)pData) = { header, { rawMouse } };
@@ -224,9 +277,8 @@ namespace games::ccj {
static bool initialized = false; static bool initialized = false;
if (initialized) { if (initialized) {
return; return;
} else {
initialized = true;
} }
initialized = true;
// announce // announce
log_info("trackball", "init"); log_info("trackball", "init");
@@ -246,8 +298,6 @@ namespace games::ccj {
} }
void trackball_thread_start() { void trackball_thread_start() {
using namespace std::chrono_literals;
tbThreadRunning = true; tbThreadRunning = true;
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE); log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
@@ -259,8 +309,9 @@ namespace games::ccj {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle); wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
} }
if (!tbThreadRunning) if (!tbThreadRunning) {
break; break;
}
timer.sleep(10); timer.sleep(10);
} }
@@ -269,8 +320,9 @@ namespace games::ccj {
void trackball_thread_stop() { void trackball_thread_stop() {
tbThreadRunning = false; tbThreadRunning = false;
if (tbThread) if (tbThread) {
tbThread->join(); tbThread->join();
}
log_info("trackball", "thread stop"); log_info("trackball", "thread stop");
+2 -4
View File
@@ -11,8 +11,8 @@
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/mme.h" #include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/detour.h" #include "util/detour.h"
@@ -653,9 +653,7 @@ namespace games::gitadora {
// monitor/touch hooks (windowed or full screen) // monitor/touch hooks (windowed or full screen)
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay // enable touch hook for subscreen overlay
wintouchemu::FORCE = true; nativetouch::hook(avs::game::DLL_INSTANCE);
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
#if !SPICE_XP #if !SPICE_XP
+2 -10
View File
@@ -20,8 +20,7 @@
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
#include "launcher/options.h" #include "launcher/options.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
@@ -63,7 +62,6 @@ namespace games::iidx {
bool TDJ_MODE = false; bool TDJ_MODE = false;
bool FORCE_720P = false; bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = 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 = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = 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 // need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE); devicehook_init(avs::core::DLL_INSTANCE);
if (NATIVE_TOUCH) { nativetouch::hook(avs::game::DLL_INSTANCE);
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);
}
} }
// insert BI2X hooks // insert BI2X hooks
-1
View File
@@ -28,7 +28,6 @@ namespace games::iidx {
extern bool TDJ_MODE; extern bool TDJ_MODE;
extern bool FORCE_720P; extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO; extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE; extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS; extern uint8_t DIGITAL_TT_SENS;
+19 -144
View File
@@ -3,30 +3,15 @@
#include <thread> #include <thread>
#include "windows.h" #include "windows.h"
#include "cfg/screen_resize.h"
#include "games/io.h" #include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "overlay/overlay.h" #include "touch/native/inject.h"
#include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.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 { namespace games::iidx::poke {
static std::thread *THREAD = nullptr; static std::thread *THREAD = nullptr;
@@ -116,80 +101,13 @@ namespace games::iidx::poke {
{"1/D", 50}, {"1/D", 50},
}; };
#if POKE_NATIVE_TOUCH static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
void initialize_native_touch_library() { if (touch_points.empty()) {
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) {
return; return;
} }
POINTER_TOUCH_INFO contact; const auto &touch = touch_points.front();
BOOL bRet = TRUE; nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
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();
} }
void enable() { void enable() {
@@ -210,35 +128,13 @@ namespace games::iidx::poke {
std::vector<TouchPoint> touch_points; std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0}; 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 // set variable to false to stop
while (THREAD_RUNNING) { while (THREAD_RUNNING) {
// clean up touch from last frame // clean up touch from last frame
if (touch_points.size() > 0) { if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH inject_native_touch_points(touch_points, false);
if (use_native) { touch_points.clear();
clear_native_touch_points(&touch_points);
} else {
clear_touch_points(&touch_points);
}
#else
clear_touch_points(&touch_points);
#endif
} }
for (int unit = 0; unit < 2; unit++) { for (int unit = 0; unit < 2; unit++) {
@@ -263,32 +159,15 @@ namespace games::iidx::poke {
float x = x_iter->second / 1920.0; float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0; float y = y_iter->second / 1080.0;
if (use_native) { if (GRAPHICS_WINDOWED) {
x *= rawinput::TOUCHSCREEN_RANGE_X; if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
y *= rawinput::TOUCHSCREEN_RANGE_Y; continue;
} 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;
} }
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
} else { } else {
// Overlay subscreen x = x_iter->second;
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH); y = y_iter->second;
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;
}
} }
TouchPoint tp { TouchPoint tp {
@@ -309,21 +188,17 @@ namespace games::iidx::poke {
} // for all units } // for all units
if (touch_points.size() > 0) { if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH inject_native_touch_points(touch_points, true);
if (use_native) {
emulate_native_touch_points(&touch_points);
} else {
touch_write_points(&touch_points);
}
#else
touch_write_points(&touch_points);
#endif
} }
// slow down // slow down
timer.sleep(50); timer.sleep(50);
} }
if (!touch_points.empty()) {
inject_native_touch_points(touch_points, false);
}
return nullptr; return nullptr;
}); });
} }
+34
View File
@@ -44,6 +44,7 @@ namespace games {
static bool IO_INITIALIZED = false; static bool IO_INITIALIZED = false;
static std::vector<std::string> games; 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;
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_keypads;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay; static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
static robin_hood::unordered_map<std::string, std::string> buttons_help; static robin_hood::unordered_map<std::string, std::string> buttons_help;
@@ -365,6 +366,37 @@ namespace games {
return it->second; 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) { static std::vector<Button> gen_buttons_keypads(const std::string &game) {
auto buttons = GameAPI::Buttons::getButtons(game); auto buttons = GameAPI::Buttons::getButtons(game);
std::vector<std::string> names; std::vector<std::string> names;
@@ -474,6 +506,8 @@ namespace games {
vkey_defaults.push_back(VK_F12); vkey_defaults.push_back(VK_F12);
names.emplace_back("Toggle Camera Control"); names.emplace_back("Toggle Camera Control");
vkey_defaults.push_back(0xFF); vkey_defaults.push_back(0xFF);
names.emplace_back("Toggle OBS Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 1 PIN Macro"); names.emplace_back("Player 1 PIN Macro");
vkey_defaults.push_back(0xFF); vkey_defaults.push_back(0xFF);
names.emplace_back("Player 2 PIN Macro"); names.emplace_back("Player 2 PIN Macro");
+12
View File
@@ -5,6 +5,16 @@
namespace games { namespace games {
namespace ModifierButtons {
enum {
Modifier1,
Modifier2,
Modifier3,
Modifier4,
Size,
};
}
namespace OverlayButtons { namespace OverlayButtons {
enum { enum {
Screenshot, Screenshot,
@@ -23,6 +33,7 @@ namespace games {
ToggleScreenResize, ToggleScreenResize,
ToggleFps, ToggleFps,
ToggleCameraControl, ToggleCameraControl,
ToggleOBSControl,
TriggerPinMacroP1, TriggerPinMacroP1,
TriggerPinMacroP2, TriggerPinMacroP2,
ScreenResize, ScreenResize,
@@ -58,6 +69,7 @@ namespace games {
const std::vector<std::string> &get_games(); const std::vector<std::string> &get_games();
std::vector<Button> *get_buttons(const std::string &game); 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_buttons_help(const std::string &game);
std::string get_analogs_help(const std::string &game); std::string get_analogs_help(const std::string &game);
std::vector<Button> *get_buttons_keypads(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 <windows.h>
#include <filesystem> #include <filesystem>
#include "avs/game.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "hooks/graphics/graphics.h"
#include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.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 { namespace games::jb {
// touch stuff // fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
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
*/
static BOOL __stdcall network_addr_is_changed() { static BOOL __stdcall network_addr_is_changed() {
return 0; return 0;
} }
/* // fixes lag spikes from the periodic ping to "eamuse.konami.fun"
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
*/
static BOOL __stdcall network_get_network_check_info() { static BOOL __stdcall network_get_network_check_info() {
return 0; return 0;
} }
/* // fixes network errors on non-DHCP interfaces
* to fix network error on non DHCP interfaces
*/
static BOOL __cdecl network_get_dhcp_result() { static BOOL __cdecl network_get_dhcp_result() {
return 1; return 1;
} }
@@ -248,23 +60,7 @@ namespace games::jb {
void JBGame::attach() { void JBGame::attach() {
Game::attach(); Game::attach();
// enable touch touch_attach();
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;
}
// enable debug logging of gftools // enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll"); HMODULE gftools = libutils::try_module("gftools.dll");
@@ -284,7 +80,6 @@ namespace games::jb {
void JBGame::detach() { void JBGame::detach() {
Game::detach(); Game::detach();
// disable touch touch_detach();
TOUCH_ENABLE = false;
} }
} }
+1 -11
View File
@@ -1,20 +1,10 @@
#pragma once #pragma once
#include "games/game.h" #include "games/game.h"
#include "games/jb/jb_touch.h"
namespace games::jb { 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 { class JBGame : public games::Game {
public: public:
JBGame(); 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/utils.h"
#include "util/execexe.h" #include "util/execexe.h"
#include "acioemu/handle.h" #include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "bi2a_hook.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 "nost.h"
#include "poke.h" #include "poke.h"
#include "touch_mode.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
#include "misc/wintouchemu.h" #include "touch/native/nativetouchhook.h"
#include "avs/game.h" #include "avs/game.h"
namespace games::nost { namespace games::nost {
bool ENABLE_POKE = false; bool ENABLE_POKE = false;
bool ENABLE_TOUCH_MODE = false;
NostGame::NostGame() : Game("Nostalgia") { NostGame::NostGame() : Game("Nostalgia") {
} }
@@ -43,9 +41,10 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE); setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings); setupapihook_add(touch_settings);
// custom touch nativetouch::hook(avs::game::DLL_INSTANCE);
// nostalgia crashes if you inject touch events too early if (ENABLE_TOUCH_MODE) {
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20); touch_mode::enable();
}
} }
void NostGame::post_attach() { void NostGame::post_attach() {
@@ -55,6 +54,9 @@ namespace games::nost {
} }
void NostGame::detach() { void NostGame::detach() {
if (ENABLE_TOUCH_MODE) {
touch_mode::disable();
}
if (ENABLE_POKE) { if (ENABLE_POKE) {
poke::disable(); poke::disable();
} }
+1
View File
@@ -5,6 +5,7 @@
namespace games::nost { namespace games::nost {
extern bool ENABLE_POKE; extern bool ENABLE_POKE;
extern bool ENABLE_TOUCH_MODE;
class NostGame : public games::Game { class NostGame : public games::Game {
public: public:
+87 -75
View File
@@ -1,22 +1,24 @@
#include "poke.h" #include "poke.h"
#include <thread> #include <atomic>
#include <optional> #include <optional>
#include <thread>
#include "games/nost/io.h" #include "games/nost/io.h"
#include "cfg/api.h" #include "cfg/api.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/touch.h" #include "touch/native/inject.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h" #include "util/precise_timer.h"
namespace games::nost::poke { namespace games::nost::poke {
static std::thread *THREAD = nullptr; static std::thread *THREAD = nullptr;
static volatile bool THREAD_RUNNING = false; static std::atomic<bool> THREAD_RUNNING { false };
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM { // positions use game-client pixels, matching the legacy client-sized touch window
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
{EAM_IO_KEYPAD_0, {760, 440}}, {EAM_IO_KEYPAD_0, {760, 440}},
{EAM_IO_KEYPAD_1, {760, 385}}, {EAM_IO_KEYPAD_1, {760, 385}},
{EAM_IO_KEYPAD_2, {820, 385}}, {EAM_IO_KEYPAD_2, {820, 385}},
@@ -31,7 +33,8 @@ namespace games::nost::poke {
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button {EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
}; };
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE { static const std::unordered_map<
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}}, {games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
{games::nost::Buttons::Button::PokeBack, {340, 100}}, {games::nost::Buttons::Button::PokeBack, {340, 100}},
{games::nost::Buttons::Button::PokeSong1, {450, 190}}, {games::nost::Buttons::Button::PokeSong1, {450, 190}},
@@ -46,13 +49,8 @@ namespace games::nost::poke {
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}}, {games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
}; };
void clear_touch_points(std::vector<TouchPoint> *touch_points) { static bool inject_poke_position(POINT position, bool down) {
std::vector<DWORD> touch_ids; return nativetouch::inject::inject_synthetic_touch(position, down);
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
} }
void enable() { void enable() {
@@ -65,124 +63,138 @@ namespace games::nost::poke {
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer; timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6; const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300; const int swipe_anim_y = 300;
const int swipe_next_page_x_begin = 1120; 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; const int swipe_prev_page_x_end = 280;
int next_page_anim_index = -1; int next_page_anim_index = -1;
int prev_page_anim_index = -1; int prev_page_anim_index = -1;
std::vector<TouchPoint> touch_points; bool contact_active = false;
bool release_pending = false;
bool touch_release = false; POINT last_position {};
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
uint16_t last_keypad_state = 0;
// log // log
log_info("poke", "enabled"); log_info("poke", "enabled");
// set variable to false to stop // set variable to false to stop
while (THREAD_RUNNING) { while (THREAD_RUNNING) {
// clean up touch from last frame if (release_pending) {
if (!touch_points.empty()) { if (contact_active) {
if (touch_release) { inject_poke_position(last_position, false);
clear_touch_points(&touch_points);
touch_release = false;
} else {
touch_points.clear();
} }
contact_active = false;
release_pending = false;
} }
auto &buttons = games::nost::get_buttons(); auto &buttons = games::nost::get_buttons();
std::optional<POINT> touch_position;
bool release_after_touch = false;
const auto button_triggered = [&](games::nost::Buttons::Button button) {
const auto pressed =
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
const auto triggered = pressed && !button_states[button];
button_states[button] = pressed;
return triggered;
};
std::optional<POINT> triggered_touch_position;
for (const auto& it : NOST_POKE_BUTTONS) {
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
}
}
const auto next_triggered =
button_triggered(games::nost::Buttons::PokeNextPage);
const auto prev_triggered =
button_triggered(games::nost::Buttons::PokePrevPage);
const auto keypad_state = eamuse_get_keypad_state(0);
if (!triggered_touch_position.has_value()) {
for (const auto& it : NOST_POKE_KEYPAD) {
const auto mask = static_cast<uint16_t>(1 << it.first);
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
break;
}
}
}
last_keypad_state = keypad_state;
if (0 <= next_page_anim_index) { if (0 <= next_page_anim_index) {
const auto delta = const auto delta =
(swipe_next_page_x_end - swipe_next_page_x_begin) (swipe_next_page_x_end - swipe_next_page_x_begin)
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames; * (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp { touch_position = POINT {
.id = touch_id, swipe_next_page_x_begin + delta,
.x = (LONG)swipe_next_page_x_begin + delta, swipe_anim_y,
.y = (LONG)swipe_anim_y,
.mouse = true,
}; };
touch_points.emplace_back(tp);
next_page_anim_index--; next_page_anim_index--;
if (next_page_anim_index < 0) { if (next_page_anim_index < 0) {
touch_release = true; release_after_touch = true;
} }
} else if (0 <= prev_page_anim_index) { } else if (0 <= prev_page_anim_index) {
const auto delta = const auto delta =
(swipe_prev_page_x_end - swipe_prev_page_x_begin) (swipe_prev_page_x_end - swipe_prev_page_x_begin)
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames; * (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp { touch_position = POINT {
.id = touch_id, swipe_prev_page_x_begin + delta,
.x = (LONG)swipe_prev_page_x_begin + delta, swipe_anim_y,
.y = (LONG)swipe_anim_y,
.mouse = true,
}; };
touch_points.emplace_back(tp);
prev_page_anim_index--; prev_page_anim_index--;
if (prev_page_anim_index < 0) { if (prev_page_anim_index < 0) {
touch_release = true; release_after_touch = true;
} }
} else { } else {
for (const auto& it : NOST_POKE) { touch_position = triggered_touch_position;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) { release_after_touch = touch_position.has_value();
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
if (!touch_release) {
const auto state = eamuse_get_keypad_state(0);
if (state) {
for (const auto& it : NOST_POKE_NUM) {
if (state & (1 << it.first)) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
}
}
// start animations for next frame // start animations for next frame
if (!touch_release) { if (!touch_position.has_value()) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) { if (next_triggered) {
next_page_anim_index = swipe_anim_total_frames; next_page_anim_index = swipe_anim_total_frames;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
if (prev_triggered) {
prev_page_anim_index = swipe_anim_total_frames; prev_page_anim_index = swipe_anim_total_frames;
} }
} }
} }
if (!touch_points.empty()) { if (touch_position.has_value() &&
touch_write_points(&touch_points); inject_poke_position(*touch_position, true)) {
contact_active = true;
last_position = *touch_position;
}
if (release_after_touch && contact_active) {
release_pending = true;
} }
// slow down // slow down
timer.sleep(30); timer.sleep(30);
} }
if (contact_active) {
inject_poke_position(last_position, false);
}
return nullptr; return nullptr;
}); });
} }
+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/unity_player.h"
#include "util/execexe.h" #include "util/execexe.h"
#include "acioemu/handle.h" #include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
-1
View File
@@ -2,7 +2,6 @@
#include "acio/icca/icca.h" #include "acio/icca/icca.h"
#include "io.h" #include "io.h"
#include "misc/wintouchemu.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/libutils.h" #include "util/libutils.h"
+2 -12
View File
@@ -19,13 +19,11 @@
#include "util/sysutils.h" #include "util/sysutils.h"
#include "io.h" #include "io.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h"
namespace games::popn { namespace games::popn {
bool SHOW_PIKA_MONITOR_WARNING = false; bool SHOW_PIKA_MONITOR_WARNING = false;
bool NATIVE_TOUCH = false;
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
@@ -715,15 +713,7 @@ namespace games::popn {
// 00000100 0000000B 00000001 (button 9) // 00000100 0000000B 00000001 (button 9)
// set third column to 0 and it will work with BIO2 // set third column to 0 and it will work with BIO2
if (!GRAPHICS_WINDOWED) { nativetouch::hook(avs::game::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("", "Main Screen", avs::game::DLL_INSTANCE);
}
}
sysutils::hook_EnumDisplayDevicesA(); sysutils::hook_EnumDisplayDevicesA();
-1
View File
@@ -22,7 +22,6 @@ namespace games::popn {
#endif #endif
extern bool SHOW_PIKA_MONITOR_WARNING; extern bool SHOW_PIKA_MONITOR_WARNING;
extern bool NATIVE_TOUCH;
class POPNGame : public games::Game { class POPNGame : public games::Game {
public: public:
+2 -2
View File
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
Game::attach(); Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) { 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 // 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); 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); SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else { } 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 { namespace games::rb {
extern uint16_t TOUCH_SCALING; extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE; extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game { class RBGame : public games::Game {
+79 -50
View File
@@ -4,7 +4,10 @@
#include "avs/game.h" #include "avs/game.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "rawinput/touch.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "touch_debug.h"
#include "touch_defs.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
@@ -12,28 +15,38 @@
static std::string WINDOW_TITLE = "REFLEC BEAT"; static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb { namespace games::rb {
uint16_t TOUCH_SCALING = 1000; static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
uint8_t TOUCH_SIZE = 1; 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) { games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
this->log_fps = log_fps;
} }
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) { 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. // read() contracts X uniformly so screen edges land within usable sensors 2..45
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it int grid_x = CLAMP(
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47); (cursor_x * X_SENSOR_COUNT) / window_width,
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74); 0,
X_SENSOR_COUNT - 1);
int grid_y = CLAMP(
(cursor_y * Y_SENSOR_COUNT) / window_height,
0,
Y_SENSOR_COUNT - 1);
// get bit positions // get bit positions
int bit_x = 88 + grid_x; int bit_x = X_SENSOR_FIRST_BIT + grid_x;
int bit_y = 74 - grid_y; int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
// insert bits packet_set_bit(data, bit_x);
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8); packet_set_bit(data, bit_y);
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
} }
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) { bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
if (wnd != nullptr) { 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 // reset window process to make the game not crash
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc)); SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
// update window // update window
SetWindowPos(wnd, nullptr, 0, 0, 0, 0, SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}
// create touch window // request automatic aspect ratio fixes
touch_create_wnd(wnd); ::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else { // create touch window
touch_create_wnd(wnd);
// create touch window // show fullscreen game window because it lost focus
touch_create_wnd(wnd); if (!GRAPHICS_WINDOWED) {
// show game window because it lost focus
ShowWindow(wnd, SW_SHOW); ShowWindow(wnd, SW_SHOW);
} }
} else { } else {
// fallback to dx hook // fallback to dx hook
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
ShowCursor(TRUE); ShowCursor(TRUE);
} }
touch_debug_attach();
return true; return true;
} }
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
// check buffer size // check buffer size
if (nNumberOfBytesToRead < 20) { if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
return 0; return 0;
} }
// get window // size against the game window cached in open() (not GetForegroundWindow), so
HWND window = GetForegroundWindow(); // touch stays correctly scaled and keeps working when the game loses focus
HWND window = this->game_hwnd;
if (window == nullptr) { if (window == nullptr) {
return 0; return 0;
} }
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
} }
// create data // create data
unsigned char data[20] {}; unsigned char data[TOUCH_PACKET_SIZE] {};
// data header // data header
data[0] = 0x55; data[0] = 0x55;
data[2] = 0x4C; 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 // iterate all touch points
auto offset_x = (int) (window_width / 48.0 / 3.0); int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
auto offset_y = (int) (window_height / 76.0 / 3.0); int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
std::vector<TouchPoint> touch_points; std::vector<TouchPoint> touch_points;
touch_get_points(touch_points); touch_get_points(touch_points);
for (auto &point : touch_points) { for (auto &point : touch_points) {
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
} }
// apply scaling // apply scaling
x = x - (window_width * (1.f - SCALE_FACTOR) / 2); x = x - (window_width * (1.f - scale_factor) / 2);
x = x / SCALE_FACTOR; x = x / scale_factor;
y = y - (window_height * (1.f - SCALE_FACTOR) / 2); y = y - (window_height * (1.f - scale_factor) / 2);
y = y / SCALE_FACTOR; y = y / scale_factor;
if (x < 0 || window_width <= x || y < 0 || window_height <= y) { if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
continue; continue;
} }
// point scaling // point coordinates
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0); // keep the verified left edge fixed while adding up to one-third of a beam
const auto point_y = (int) (y - window_height / 76); // 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 // model a finger as a 3x3 block of IR sensors around the touch point
grid_insert(data, point_x, point_y); // this gives better accuracy (than just 1x1) since the logic below
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
// surrounding points // the center point, which means by inserting up to 8 extra blocks
if (games::rb::TOUCH_SIZE == 3) { // we are emulating a sub-"pixel" resolution
grid_insert(data, point_x - offset_x, point_y); // west grid_insert(data, point_x, point_y); // center
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x + offset_x, point_y); // east grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x, point_y - offset_y); // north grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y + offset_y); // south 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 // copy data to buffer
memcpy(lpBuffer, data, 20); memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
// update frame logging // update frame logging
if (log_fps) { if (log_fps) {
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
} }
// return amount of bytes written // return amount of bytes written
return 20; return TOUCH_PACKET_SIZE;
} }
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) { 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() { bool games::rb::ReflecBeatTouchDeviceHandle::close() {
touch_debug_detach();
// detach touch module // detach touch module
touch_detach(); touch_detach();
+6
View File
@@ -12,6 +12,12 @@ namespace games::rb {
int window_width = 1080; int window_width = 1080;
int window_height = 1920; 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 // logging
bool log_fps = false; bool log_fps = false;
uint64_t log_time = 0; 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/graphics/graphics.h"
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
#include "hooks/libraryhook.h" #include "hooks/libraryhook.h"
#ifdef SPICE64
#include "hooks/graphics/nvapi_impl.h"
#endif
#include "hooks/graphics/nvapi_hook.h" #include "hooks/graphics/nvapi_hook.h"
#include "hooks/powrprof.h" #include "hooks/powrprof.h"
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
@@ -23,8 +26,7 @@
#include "util/libutils.h" #include "util/libutils.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "camera.h" #include "camera.h"
#include "io.h" #include "io.h"
@@ -46,7 +48,6 @@ namespace games::sdvx {
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)"; const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
// settings // settings
bool NATIVETOUCH = false;
uint8_t DIGITAL_KNOB_SENS = 16; uint8_t DIGITAL_KNOB_SENS = 16;
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM; SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
bool ENABLE_COM_PORT_SCAN_HOOK = false; bool ENABLE_COM_PORT_SCAN_HOOK = false;
@@ -428,29 +429,31 @@ namespace games::sdvx {
if (aio != nullptr) { if (aio != nullptr) {
// enable 9on12 for AMD // enable 9on12 for AMD
if (!libutils::try_library("nvapi64.dll")) { const auto nvapi = libutils::try_library("nvapi64.dll");
if (nvapi == nullptr) {
log_info( log_info(
"sdvx", "sdvx",
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled"); "nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true; 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 { } else {
// don't let nvapi mess with display settings // don't let nvapi mess with display settings
nvapi_hook::initialize(avs::game::DLL_INSTANCE); nvapi_hook::initialize(avs::game::DLL_INSTANCE);
} }
if (is_valkyrie_model()) { if (is_valkyrie_model()) {
if (NATIVETOUCH) { nativetouch::hook(avs::game::DLL_INSTANCE);
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);
}
// insert BI2X hooks // insert BI2X hooks
bi2x_hook_init(); bi2x_hook_init();
-1
View File
@@ -18,7 +18,6 @@ namespace games::sdvx {
}; };
// settings // settings
extern bool NATIVETOUCH;
extern uint8_t DIGITAL_KNOB_SENS; extern uint8_t DIGITAL_KNOB_SENS;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern SdvxOverlayPosition OVERLAY_POS; extern SdvxOverlayPosition OVERLAY_POS;
+72
View File
@@ -0,0 +1,72 @@
#include "sdvx_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds.
#ifdef SPICE64
#include <string>
#include "hooks/graphics/graphics.h"
#include "launcher/logger.h"
#include "util/logging.h"
namespace games::sdvx {
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
//
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
// Live2D rendering); we watch those log lines and keep the shared flag
// the d3d9 backend reads up to date. the hook never alters the log output
// (always returns false).
static bool live2d_scene_log_hook(
void *user, const std::string &data, logger::Style style, std::string &out) {
// any of these scenes counts as in-song gameplay (different play modes)
static const char *const gameplay_scenes[] = {
"in ALTERNATIVE_GAME_SCENE",
"in MEGAMIX_GAME_SCENE",
"in MEGAMIX_BATTLE",
"in BATTLE_GAME_SCENE",
"in AUTOMATION_GAME_SCENE",
"in ARENA_GAME_SCENE",
};
bool in_gameplay_scene = false;
for (const auto *scene : gameplay_scenes) {
if (data.find(scene) != std::string::npos) {
in_gameplay_scene = true;
break;
}
}
if (!in_gameplay_scene) {
return false;
}
// note: log messages here must NOT contain any matched scene token, else
// this hook would re-enter itself when the message is pushed.
if (data.find("I:Attach: in ") != std::string::npos) {
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: entering gameplay");
}
} else if (data.find("I:Detach: in ") != std::string::npos) {
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: leaving gameplay");
}
}
return false;
}
void live2d_scene_detection_init() {
static bool installed = false;
if (installed) {
return;
}
installed = true;
// the logger's hook list is a persistent static, so registering here is
// safe even though this runs before logger::start(). we intentionally do
// NOT log a confirmation now: at this point the log file isn't open yet
// and the message would be dropped. the entering/leaving-gameplay lines
// above provide runtime confirmation once the logger is running.
logger::hook_add(live2d_scene_log_hook, nullptr);
}
}
#endif // SPICE64
+14
View File
@@ -0,0 +1,14 @@
#pragma once
namespace games::sdvx {
#ifdef SPICE64
// installs the Live2D in-game scene-detection log hook used by the
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
// be attached, so it can be enabled purely from the launcher option.
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
// of 32-bit builds.
void live2d_scene_detection_init();
#endif
}
+19 -9
View File
@@ -32,18 +32,28 @@ namespace hooks::audio {
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS; RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
index++) { 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 }; char desc[256] = { 0 };
DWORD size = sizeof(desc); DWORD size = sizeof(desc);
std::string name = key_name; std::string name = key_name;
if (RegGetValueA(hkEnum, HKEY hkDriver = nullptr;
key_name, if (RegOpenKeyExA(hkEnum, key_name, 0,
ASIO_REG_DESC, KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
RRF_RT_REG_SZ | wow64_flag, DWORD type = 0;
nullptr, if (RegQueryValueExA(hkDriver,
desc, ASIO_REG_DESC,
&size) == ERROR_SUCCESS && desc[0]) { nullptr,
name = desc; &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) // 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 // one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
// new instance without releasing the old one has leaked it; we track this to tear the // single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
// stale one down // 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::mutex g_wrappers_mutex;
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers; std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it WrappedAsio *find_wrapper(REFCLSID clsid) {
// replaces so the caller can tear it down outside the lock
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex); std::lock_guard lock(g_wrappers_mutex);
for (auto &entry : g_wrappers) { for (auto &entry : g_wrappers) {
if (IsEqualCLSID(entry.first, clsid)) { if (IsEqualCLSID(entry.first, clsid)) {
WrappedAsio *stale = entry.second; return entry.second;
entry.second = wrapper;
return stale;
} }
} }
g_wrappers.emplace_back(clsid, wrapper);
return nullptr; return nullptr;
} }
void unregister_wrapper(WrappedAsio *wrapper) { void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex); std::lock_guard lock(g_wrappers_mutex);
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) { g_wrappers.emplace_back(clsid, wrapper);
if (it->second == wrapper) {
g_wrappers.erase(it);
return;
}
}
} }
// ASIO drivers registered on this system (CLSID + registry name), scanned once // 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() { WrappedAsio::~WrappedAsio() {
unregister_wrapper(this);
this->detach_post_process(); this->detach_post_process();
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is // never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
// exactly one here and this releases/unloads it deterministically // 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(); this->pReal->Release();
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid)); log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
#pragma region IAsio #pragma region IAsio
AsioBool __thiscall WrappedAsio::init(void *sys_handle) { 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); const AsioBool result = this->pReal->init(sys_handle);
if (result == AsioTrue) { if (result == AsioTrue) {
this->initialized = true;
log_info( log_info(
"audio::wrappedasio", "audio::wrappedasio",
"init succeeded for '{}' (driver version {})", "init succeeded for '{}' (driver version {})",
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
AsioError __thiscall WrappedAsio::start() { AsioError __thiscall WrappedAsio::start() {
const AsioError result = this->pReal->start(); const AsioError result = this->pReal->start();
if (result == ASE_OK) { if (result == ASE_OK) {
this->started = true;
log_info( log_info(
"audio::wrappedasio", "audio::wrappedasio",
"start succeeded, ASIO stream is now running on '{}'", "start succeeded, ASIO stream is now running on '{}'",
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
AsioError __thiscall WrappedAsio::stop() { AsioError __thiscall WrappedAsio::stop() {
const AsioError result = this->pReal->stop(); const AsioError result = this->pReal->stop();
if (result == ASE_OK) { if (result == ASE_OK) {
this->started = false;
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name); log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
} else { } else {
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result)); 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); 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) { void WrappedAsio::apply_output_volume(long double_buffer_index) {
if (double_buffer_index != 0 && double_buffer_index != 1) { if (double_buffer_index != 0 && double_buffer_index != 1) {
return; return;
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
} }
} }
this->publish_post_process(buffer_size); this->publish_post_process(buffer_size);
this->buffers_created = true;
} else { } else {
log_warning( log_warning(
"audio::wrappedasio", "audio::wrappedasio",
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
// device's 2.0 output. then publish ourselves to the realtime thread // device's 2.0 output. then publish ourselves to the realtime thread
this->record_downmix_channels(buffer_infos, num_channels, buffer_size); this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
this->publish_post_process(buffer_size); this->publish_post_process(buffer_size);
this->buffers_created = true;
log_info( log_info(
"audio::wrappedasio", "audio::wrappedasio",
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
this->volume_channels.clear(); this->volume_channels.clear();
this->volume_active = false; this->volume_active = false;
this->downmix_active = false; this->downmix_active = false;
this->buffers_created = false;
return result; return result;
} }
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
auto *wrapper = new WrappedAsio( auto *wrapper = new WrappedAsio(
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid)); reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
// if the host already had a live wrapper for this CLSID it leaked the previous // pin the wrapper (and the real driver it owns) for the process lifetime; later
// instance (ASIO is single-instance); tear it down now so the real driver is // CoCreate calls reuse this same instance via wrap_existing
// released before the reinit. works around games (iidx32+) that ref twice but wrapper->AddRef();
// deref once before re-initializing. store_wrapper(clsid, wrapper);
//
// 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;
}
return static_cast<IAsio *>(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); bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and // 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); 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 // 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; AsioError __thiscall output_ready() override;
#pragma endregion #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: private:
// create_buffers implementation used when a stereo extraction is active: forwards only // 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 // 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 // unambiguous name; constant for our lifetime
std::string driver_name; 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 // our own reference count; we hold one reference on pReal and release it when this
// drops to zero // drops to zero
std::atomic<ULONG> ref_count {1}; std::atomic<ULONG> ref_count {1};
+59 -5
View File
@@ -7,6 +7,7 @@
#include <audioclient.h> #include <audioclient.h>
#include <mmdeviceapi.h> #include <mmdeviceapi.h>
#include "avs/game.h"
#include "hooks/audio/backends/mmdevice/device_enumerator.h" #include "hooks/audio/backends/mmdevice/device_enumerator.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
@@ -15,6 +16,7 @@
#include "audio_private.h" #include "audio_private.h"
#include "acm.h" #include "acm.h"
#include "asio_proxy.h" #include "asio_proxy.h"
#include "xact.h"
#ifdef _MSC_VER #ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator, DEFINE_GUID(CLSID_MMDeviceEnumerator,
@@ -53,9 +55,39 @@ namespace hooks::audio {
std::string ASIO_DRIVER_NAME = ""; std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false; bool ASIO_FORCE_UNLOAD_ON_STOP = false;
// private globals
IAudioClient *CLIENT = nullptr;
std::mutex INITIALIZE_LOCK; // for asio 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( static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
@@ -68,6 +100,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
HRESULT ret; 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 // call original
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv); ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) { if (FAILED(ret)) {
@@ -120,15 +161,28 @@ namespace hooks::audio {
// general hooks // general hooks
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook); CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
if (avs::game::is_model("PAN")) {
hooks::audio::xact::init();
}
} }
void stop() { void stop() {
log_info("audio", "stopping"); log_info("audio", "stopping");
if (CLIENT) { IAudioClient *client = nullptr;
CLIENT->Stop(); {
CLIENT->Release(); std::lock_guard lock(CLIENT_LOCK);
CLIENT_STOPPING = true;
client = CLIENT;
CLIENT = nullptr; CLIENT = nullptr;
} }
if (client) {
client->Stop();
client->Release();
}
stop_low_latency(); 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 std::mutex INITIALIZE_LOCK;
extern bool VOLUME_HOOK_ENABLED; extern bool VOLUME_HOOK_ENABLED;
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT; 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); 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; IAudioClient *client = nullptr;
if (iid == IID_IAudioClient) { if (iid == IID_IAudioClient) {
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface)); client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface)); client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
} }
*ppInterface = client; *ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) { } else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface)); *ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid)); log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
if (iid == IID_IAudioClient) { 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())); auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
*ppInterface = client; *ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
return S_OK; return S_OK;
} }
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
device_format->nChannels * (device_format->wBitsPerSample / 8)); device_format->nChannels * (device_format->wBitsPerSample / 8));
} }
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
return ret; return ret;
} }
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) { 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)); log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat); copy_wave_format(&hooks::audio::FORMAT, pFormat);
copy_wave_format(&this->device_format, pFormat); copy_wave_format(&this->device_format, pFormat);
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
return ret; return ret;
} }
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
log_info("audio::wasapi", "IAudioClient::Initialize hook hit"); log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
CHECK_RESULT(this->backend->on_initialize( HRESULT ret = this->backend->on_initialize(
&ShareMode, &ShareMode,
&StreamFlags, &StreamFlags,
&hnsBufferDuration, &hnsBufferDuration,
&hnsPeriodicity, &hnsPeriodicity,
pFormat, pFormat,
AudioSessionGuid)); AudioSessionGuid);
if (SUCCEEDED(ret)) {
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
}
CHECK_RESULT(ret);
} }
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) { HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
static std::once_flag printed; 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();
}
@@ -1273,9 +1273,11 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
wintouchemu::update(); wintouchemu::update();
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW // newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen // (when enabled on older versions of EG, you end up with graphical glitches on the subscreen)
// early versions of popn HC needs this as well, otherwise the subscreen doesn't update at all //
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW || games::popn::is_pikapika_model()) { // early versions of popn HC needs this as well, but not on by default as it can cause
// graphical glitches on some GPUs
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0); SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
} }
} }
@@ -1,7 +1,11 @@
#include "d3d9_device.h" #include "d3d9_device.h"
#include <algorithm>
#include <cassert> #include <cassert>
#include <climits>
#include <mutex> #include <mutex>
#include <unordered_map>
#include <vector>
#include "avs/game.h" #include "avs/game.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
@@ -12,6 +16,7 @@
#include "cfg/screen_resize.h" #include "cfg/screen_resize.h"
#include "d3d9_backend.h" #include "d3d9_backend.h"
#include "d3d9_live2d.h"
#include "d3d9_texture.h" #include "d3d9_texture.h"
#ifndef SPICE64 #ifndef SPICE64
@@ -1301,6 +1306,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitive(
UINT PrimitiveCount) UINT PrimitiveCount)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount)); CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
} }
@@ -1313,6 +1321,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitive(
UINT PrimitiveCount) UINT PrimitiveCount)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawIndexedPrimitive( CHECK_RESULT(pReal->DrawIndexedPrimitive(
PrimitiveType, BaseVertexIndex, PrimitiveType, BaseVertexIndex,
MinVertexIndex, NumVertices, MinVertexIndex, NumVertices,
@@ -1328,6 +1339,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitiveUP(
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})", WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
PrimitiveType, PrimitiveCount, PrimitiveType, PrimitiveCount,
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride); fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawPrimitiveUP( CHECK_RESULT(pReal->DrawPrimitiveUP(
PrimitiveType, PrimitiveCount, PrimitiveType, PrimitiveCount,
pVertexStreamZeroData, VertexStreamZeroStride)); pVertexStreamZeroData, VertexStreamZeroStride));
@@ -1344,6 +1358,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitiveUP(
UINT VertexStreamZeroStride) UINT VertexStreamZeroStride)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP( CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData, PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride)); IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
@@ -1403,7 +1420,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateVertexShader(
IDirect3DVertexShader9 **ppShader) IDirect3DVertexShader9 **ppShader)
{ {
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction)); WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
CHECK_RESULT(pReal->CreateVertexShader(pFunction, ppShader)); HRESULT ret = pReal->CreateVertexShader(pFunction, ppShader);
if (SUCCEEDED(ret) && ppShader != nullptr) {
d3d9_live2d::on_create_vertex_shader(*ppShader, pFunction);
}
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
}
return ret;
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
@@ -1411,6 +1435,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
{ {
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader)); WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
d3d9_live2d::on_set_vertex_shader(pShader);
#ifndef SPICE64 #ifndef SPICE64
// diagonal line fix // diagonal line fix
@@ -1557,13 +1583,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreatePixelShader(
IDirect3DPixelShader9 **ppShader) IDirect3DPixelShader9 **ppShader)
{ {
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction)); WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
CHECK_RESULT(pReal->CreatePixelShader(pFunction, ppShader)); HRESULT ret = pReal->CreatePixelShader(pFunction, ppShader);
if (SUCCEEDED(ret) && ppShader != nullptr) {
d3d9_live2d::on_create_pixel_shader(*ppShader, pFunction);
}
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
}
return ret;
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
IDirect3DPixelShader9 *pShader) IDirect3DPixelShader9 *pShader)
{ {
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader)); WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
d3d9_live2d::on_set_pixel_shader(pShader);
CHECK_RESULT(pReal->SetPixelShader(pShader)); CHECK_RESULT(pReal->SetPixelShader(pShader));
} }
@@ -0,0 +1,144 @@
#include "d3d9_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
#ifdef SPICE64
#include <cstdint>
#include <unordered_set>
#include "hooks/graphics/graphics.h"
// how the Live2D draw filtering works
// ------------------------------------
// SDVX draws its Live2D characters with a small, fixed set of pixel and
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at
// the exact moment the game issues a draw call. the d3d9 device hooks feed three
// kinds of events into this module:
//
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
// the game compiles its shaders once at load. we can't trust the shader
// *object pointer* to identify a shader (it's just a heap address that
// varies per run and can be recycled), so instead we hash the shader's
// D3D9 *bytecode* - that fingerprint is stable across runs because the
// game ships the same shaders. if the hash matches a known Live2D shader
// we remember that object pointer in g_live2d_shaders.
//
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
// whenever the game binds a shader we look it up in that set once and cache
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
// far less often than draws, so this is where the lookup cost lives.
//
// 3. draw call (should_skip_draw, called from every Draw* hook)
// the per-draw question "is this a Live2D draw?" is then just reading those
// two cached bools - no hashing, no map lookups. if the skip is currently
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is
// Live2D, the Draw* hook drops the call instead of forwarding it.
//
// everything is gated on the feature being enabled (mode != Off); when it's Off
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a
// device is single-threaded, so none of this state needs locking.
namespace {
// shader state is tracked whenever the feature might act (mode != Off) so the
// known-shader set is populated before a song starts. when Off, every entry
// point is a single cheap branch.
bool tracking_enabled() {
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
}
// the set of shader objects (pixel or vertex) whose bytecode matched a known
// Live2D fingerprint. only matching shaders are stored, so this stays tiny.
std::unordered_set<void *> g_live2d_shaders;
// whether the currently-bound shaders are known Live2D shaders. cached at set
// time so the per-draw check is just two bool reads.
bool g_cur_ps_is_live2d = false;
bool g_cur_vs_is_live2d = false;
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
uint64_t bytecode_hash(const DWORD *func) {
if (func == nullptr) {
return 0;
}
const DWORD *p = func;
const DWORD *cap = func + 65536; // safety bound
while (p < cap && *p != 0x0000FFFF) {
p++;
}
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
uint64_t h = 1469598103934665603ULL;
const auto *bytes = reinterpret_cast<const uint8_t *>(func);
for (size_t i = 0; i < n_bytes; i++) {
h ^= bytes[i];
h *= 1099511628211ULL;
}
return h;
}
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
// across runs because the game ships fixed shaders. the two sets are disjoint so
// a single shader can be classified by its own hash alone.
bool hash_is_live2d(uint64_t hash) {
switch (hash) {
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
case 0xe9cf898c331e2a51ULL: // vertex
case 0x94dc84e7b7c0f437ULL: // vertex
case 0xc872937c5cc04309ULL: // vertex
return true;
}
return false;
}
// classify a shader at creation time and record it if it is Live2D. erasing on a
// miss keeps the set correct if the runtime reuses a freed shader pointer.
void classify_shader(void *shader, const DWORD *func) {
if (hash_is_live2d(bytecode_hash(func))) {
g_live2d_shaders.insert(shader);
} else {
g_live2d_shaders.erase(shader);
}
}
} // namespace
namespace d3d9_live2d {
// stage 1: fingerprint each shader as the game creates it
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func);
}
}
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func);
}
}
// stage 2: remember whether the just-bound shader is a Live2D one
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
if (tracking_enabled()) [[unlikely]] {
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
}
}
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
if (tracking_enabled()) [[unlikely]] {
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
}
}
// stage 3: drop the draw if the skip is active and a Live2D shader is bound
bool should_skip_draw() {
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
}
} // namespace d3d9_live2d
#endif // SPICE64
@@ -0,0 +1,44 @@
#pragma once
#include <windows.h>
#include <d3d9.h>
// SDVX Live2D draw-skip support for the D3D9 backend.
//
// SDVX renders its Live2D navigator / in-song character through a fixed set of
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
// the matching draw calls are dropped to save GPU. shaders are identified by a
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
// does not). the hashes were captured with the draw-call fingerprinting tool.
//
// every entry point is a no-op unless the feature is enabled (mode != Off), and
// d3d9 rendering for a device is single-threaded, so none of this needs locking.
namespace d3d9_live2d {
#ifdef SPICE64
// record a shader's bytecode fingerprint at creation time
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
// remember the currently-bound shaders
void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
// true if the current draw call should be dropped (skip active AND the bound
// shaders identify it as SDVX Live2D)
bool should_skip_draw();
#else // !SPICE64
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
// call sites.
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
inline bool should_skip_draw() { return false; }
#endif // SPICE64
}
+33 -26
View File
@@ -23,12 +23,12 @@
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/touch_indicators.h" #include "touch/touch_gestures.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h" #include "touch/native/inject.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
@@ -86,6 +86,10 @@ static bool monitor_layout_needs_reset = false;
bool GRAPHICS_CAPTURE_CURSOR = false; bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false; bool GRAPHICS_LOG_HRESULT = false;
bool GRAPHICS_SDVX_FORCE_720 = false; bool GRAPHICS_SDVX_FORCE_720 = false;
#ifdef SPICE64
SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Off;
std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY = false;
#endif // SPICE64
bool GRAPHICS_SHOW_CURSOR = false; bool GRAPHICS_SHOW_CURSOR = false;
bool GRAPHICS_WINDOWED = false; bool GRAPHICS_WINDOWED = false;
std::vector<HWND> GRAPHICS_WINDOWS; std::vector<HWND> GRAPHICS_WINDOWS;
@@ -184,11 +188,13 @@ static bool gitadora_should_block_game_window_placement(HWND hWnd) {
} }
const auto window_name = gitadora_window_name_for_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) { 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; GFDM_LEFT_WINDOW = hWnd;
} else if (window_name == "RIGHT") { } else if (window_name == "RIGHT") {
GFDM_RIGHT_WINDOW = hWnd; GFDM_RIGHT_WINDOW = hWnd;
@@ -301,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 // window resize
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam); graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
@@ -585,6 +575,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
hWndParent, hMenu, hInstance, lpParam); hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result); 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) // theme the native title bar (dark/light)
set_window_dark_titlebar(result); set_window_dark_titlebar(result);
@@ -605,9 +601,6 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
// only hook touch window if multiple windows are allowed // 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) { if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_remember_window(result, gfdm_window_name); gitadora_remember_window(result, gfdm_window_name);
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
@@ -623,7 +616,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
disable_touch_indicators(result); disable_touch_gestures(result);
log_misc( log_misc(
"graphics", "graphics",
"CreateWindowExA returned {}, {}", "CreateWindowExA returned {}, {}",
@@ -718,7 +711,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
fmt::ptr(result), fmt::ptr(result),
lpWindowName ? ws2s(lpWindowName) : "(null)"); lpWindowName ? ws2s(lpWindowName) : "(null)");
disable_touch_indicators(result); disable_touch_gestures(result);
return result; return result;
} }
@@ -793,7 +786,7 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nWidth = rect.right - rect.left; nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top; nHeight = rect.bottom - rect.top;
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW); nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
} else { } else {
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead // Existing behaviour: suppress subscreen window and prompt user to use overlay instead
log_info( log_info(
@@ -946,6 +939,13 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
return true; 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 // call original
return ShowWindow_orig(hWnd, nCmdShow); return ShowWindow_orig(hWnd, nCmdShow);
} }
@@ -1159,6 +1159,13 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc)); 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 // NOLEGACY causes WM_CHAR to be not received
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch) // 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"}))) { if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
+31 -1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <atomic>
#include <string> #include <string>
#include <vector> #include <vector>
#include <optional> #include <optional>
@@ -27,6 +28,33 @@ enum graphics_dx9on12_state {
DX9ON12_FORCE_ON, DX9ON12_FORCE_ON,
}; };
// SDVX Live2D suppression policy. the mode comes from the launcher
// option; the in-gameplay flag is maintained by the SDVX scene-detection hook in
// games/sdvx (which does not require the SDVX game module to be enabled). the
// d3d9 backend reads graphics_sdvx_live2d_should_skip() on every draw.
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds.
#ifdef SPICE64
enum class SdvxLive2dMode {
Off, // leave Live2D untouched (default)
Always, // always skip Live2D draws (also removes the menu navigator)
InGame, // skip Live2D draws only during a song
};
extern SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE;
extern std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY;
inline bool graphics_sdvx_live2d_should_skip() {
switch (GRAPHICS_SDVX_LIVE2D_MODE) {
case SdvxLive2dMode::Always:
return true;
case SdvxLive2dMode::InGame:
return GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.load(std::memory_order_relaxed);
default:
return false;
}
}
#endif // SPICE64
// flag settings // flag settings
extern bool GRAPHICS_CAPTURE_CURSOR; extern bool GRAPHICS_CAPTURE_CURSOR;
extern bool GRAPHICS_LOG_HRESULT; extern bool GRAPHICS_LOG_HRESULT;
@@ -60,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR; extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR; extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
extern std::string GRAPHICS_GITADORA_SMALL_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 bool GRAPHICS_IIDX_WSUB;
extern std::optional<std::string> GRAPHICS_WSUB_SIZE; extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
@@ -129,7 +159,7 @@ bool graphics_gitadora_apply_window_monitor(
int &width, int &width,
int &height, int &height,
bool log_change); 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 change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot( 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_LEFT_MONITOR;
std::string GRAPHICS_GITADORA_RIGHT_MONITOR; std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
std::string GRAPHICS_GITADORA_SMALL_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 // IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
bool GRAPHICS_IIDX_WSUB = false; bool GRAPHICS_IIDX_WSUB = false;
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
return empty; 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); 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( static bool graphics_monitor_rect_from_name(
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
int &width, int &width,
int &height, int &height,
bool log_change) { 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), graphics_gitadora_monitor_for_window_name(window_name),
window_name, window_name,
x, x,
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
width, width,
height, height,
log_change); 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) { 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_width = client_w;
cfg::SCREENRESIZE->init_client_height = client_h; cfg::SCREENRESIZE->init_client_height = client_h;
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h; cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
log_debug( log_misc(
"graphics-windowed", "graphics-windowed",
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}", "[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
fmt::ptr(hWnd), fmt::ptr(hWnd),
+66 -1
View File
@@ -1,5 +1,6 @@
#include "external/nvapi/nvapi.h" #include "external/nvapi/nvapi.h"
#include "external/nvapi/NvApiDriverSettings.h" #include "external/nvapi/NvApiDriverSettings.h"
#include "avs/game.h"
#include "hooks/libraryhook.h" #include "hooks/libraryhook.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
@@ -12,14 +13,21 @@ namespace nvapi_hook {
bool BYPASS_NVAPI = false; bool BYPASS_NVAPI = false;
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int); typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr; 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 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( static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags); NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
void initialize(HINSTANCE dll) { void initialize(HINSTANCE dll) {
#ifdef SPICE64 #ifdef SPICE64
std::string nvapi_dll = "nvapi64.dll"; std::string nvapi_dll = "nvapi64.dll";
#else #else
@@ -40,6 +48,18 @@ namespace nvapi_hook {
return nullptr; 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 // NvAPI_DISP_SetDisplayConfig
if (func_code == 0x5D8CF8DE) { if (func_code == 0x5D8CF8DE) {
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked"); log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
@@ -51,8 +71,53 @@ namespace nvapi_hook {
return NvAPI_QueryInterface_orig(func_code); 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( NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) { 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"); log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
return NVAPI_OK; 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(LoadLibraryA) *LoadLibraryA_orig = nullptr;
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr; static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr; static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr; static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr; static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
return LoadLibraryW_orig(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) { static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
// check hooks // check hooks
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
// detour // detour
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig); 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", "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", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig); detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig); detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
+109 -29
View File
@@ -49,10 +49,12 @@
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "games/qma/qma.h" #include "games/qma/qma.h"
#include "games/rb/rb.h" #include "games/rb/rb.h"
#include "games/rb/touch_debug.h"
#include "games/rf3d/rf3d.h" #include "games/rf3d/rf3d.h"
#include "games/sc/sc.h" #include "games/sc/sc.h"
#include "games/scotto/scotto.h" #include "games/scotto/scotto.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "games/sdvx/sdvx_live2d.h"
#include "games/shared/printer.h" #include "games/shared/printer.h"
#include "games/silentscope/silentscope.h" #include "games/silentscope/silentscope.h"
#include "games/mfc/mfc.h" #include "games/mfc/mfc.h"
@@ -100,8 +102,9 @@
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.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/iidx_seg.h"
#include "overlay/windows/obs.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "rawinput/touch.h" #include "rawinput/touch.h"
#include "reader/reader.h" #include "reader/reader.h"
@@ -376,6 +379,7 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) { if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true; GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
} }
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) { if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true; GRAPHICS_FORCE_SINGLE_ADAPTER = true;
@@ -425,9 +429,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) { if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
D3D9_DEVICE_HOOK_DISABLE = true; 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 // not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
overlay::ENABLED = false; overlay::ENABLED = false;
// leaving these on without dx9hooks result in torn state and therefore failure to draw // leaving these on without dx9hooks result in torn state and therefore failure to draw
@@ -487,9 +488,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) { if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
attach_sdvx = true; attach_sdvx = true;
} }
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
games::sdvx::NATIVETOUCH = true;
}
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) { if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t) games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32(); options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
@@ -590,9 +588,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) { if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
games::iidx::DISABLE_ESPEC_IO = true; 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 // should come later since this will override a few settings
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() || if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) { (options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
@@ -640,8 +635,8 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) { if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text(); sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
} }
if (options[launcher::Options::PopnNativeTouch].value_bool()) { if (options[launcher::Options::PopnSubRedraw].value_bool()) {
games::popn::NATIVE_TOUCH = true; SUBSCREEN_FORCE_REDRAW = true;
} }
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) { if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
attach_mga = true; attach_mga = true;
@@ -883,7 +878,7 @@ int main_implementation(int argc, char *argv[]) {
options[launcher::Options::ScreenResizeConfigPath].value_text(); options[launcher::Options::ScreenResizeConfigPath].value_text();
} }
if (options[launcher::Options::PatchManagerConfigPath].is_active()) { 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(); options[launcher::Options::PatchManagerConfigPath].value_text();
} }
if (options[launcher::Options::PathToAppConfig].is_active()) { if (options[launcher::Options::PathToAppConfig].is_active()) {
@@ -1042,9 +1037,32 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::SDVXForce720p].value_bool()) { if (options[launcher::Options::SDVXForce720p].value_bool()) {
GRAPHICS_SDVX_FORCE_720 = true; GRAPHICS_SDVX_FORCE_720 = true;
} }
#ifdef SPICE64
// only the Live2D-capable SDVX versions are 64-bit, so this whole feature is
// gated out of 32-bit builds and only armed for SDVX (model KFC).
if (avs::game::is_model("KFC")) {
auto live2d = options[launcher::Options::SDVXDisableLive2D].value_text();
if (live2d == "always") {
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Always;
} else if (live2d == "ingame") {
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::InGame;
// scene detection runs independently of the SDVX game module
games::sdvx::live2d_scene_detection_init();
}
}
#endif // SPICE64
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) { if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
rawinput::touch::INVERTED = true; 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 // DisableTouchCardInsert is no longer honored in spice2x
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) { // if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
// SPICETOUCH_CARD_DISABLE = true; // SPICETOUCH_CARD_DISABLE = true;
@@ -1086,6 +1104,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::DisableOverlay].value_bool()) { if (options[launcher::Options::DisableOverlay].value_bool()) {
overlay::ENABLED = false; 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) { if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
const auto val = options[launcher::Options::OverlayScaling].value_uint32(); const auto val = options[launcher::Options::OverlayScaling].value_uint32();
if (10 <= val && val <= 400 && val != 100) { if (10 <= val && val <= 400 && val != 100) {
@@ -1254,6 +1275,14 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_GITADORA_SMALL_MONITOR = GRAPHICS_GITADORA_SMALL_MONITOR =
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text(); 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 // IIDX/SDVX Windowed Subscreen
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) { if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
@@ -1286,21 +1315,36 @@ int main_implementation(int argc, char *argv[]) {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate; games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") { } else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::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 { } else {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved; 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 // reflec beat touch emulation
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) { if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
} }
if (options[launcher::Options::RBTouchSize].is_active()) { // -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
if (options[launcher::Options::RBTouchPollRate].is_active()) { if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32(); games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
} }
@@ -1434,6 +1478,26 @@ int main_implementation(int argc, char *argv[]) {
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true; cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
} }
// OBS WebSocket overlay settings
if (options[launcher::Options::OBSWebSocketEnabled].value_bool()) {
overlay::windows::OBS_CONTROL_ENABLED = true;
}
if (options[launcher::Options::OBSWebSocketHost].is_active()) {
overlay::windows::OBS_CONTROL_HOST = options[launcher::Options::OBSWebSocketHost].value_text();
}
if (options[launcher::Options::OBSWebSocketPort].is_active()) {
const auto obs_port = options[launcher::Options::OBSWebSocketPort].value_uint32();
if (obs_port > 0 && obs_port <= 65535) {
overlay::windows::OBS_CONTROL_PORT = static_cast<uint16_t>(obs_port);
}
}
if (options[launcher::Options::OBSWebSocketPassword].is_active()) {
overlay::windows::OBS_CONTROL_PASSWORD = options[launcher::Options::OBSWebSocketPassword].value_text();
}
if (options[launcher::Options::OBSWebSocketDebug].value_bool()) {
overlay::windows::OBS_CONTROL_DEBUG = true;
}
// API debugging // API debugging
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) { if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty); API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
@@ -1546,8 +1610,19 @@ int main_implementation(int argc, char *argv[]) {
// print out conflicts // print out conflicts
size_t conflicts = 0; size_t conflicts = 0;
for (const auto &option : options) { for (size_t i = 0; i < options.size(); i++) {
if (option.conflicting && option.get_definition().type != OptionType::Bool) { // InjectHook / EarlyInjectHook accept multiple values, so command line and
// spicecfg entries are merged rather than conflicting; don't warn about them
if (i == (size_t) launcher::Options::InjectHook ||
i == (size_t) launcher::Options::EarlyInjectHook) {
continue;
}
const auto &option = options[i];
// ignore Boolean values
if (option.get_definition().type == OptionType::Bool) {
continue;
}
if (option.conflicting) {
conflicts += 1; conflicts += 1;
const auto& value = option.get_definition().sensitive ? "*****" : option.value; const auto& value = option.get_definition().sensitive ? "*****" : option.value;
if (launcher::USE_CMD_OVERRIDE) { if (launcher::USE_CMD_OVERRIDE) {
@@ -1711,7 +1786,16 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::poke::enable(); games::iidx::poke::enable();
} }
if (options[launcher::Options::NostalgiaPoke].is_active()) { 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");
}
} }
} }
@@ -2439,10 +2523,8 @@ int main_implementation(int argc, char *argv[]) {
// copy defaults to nvram // copy defaults to nvram
avs::core::copy_defaults(); avs::core::copy_defaults();
// prepare patches // prepare patches (registers the DLL-load notification before the game DLL loads)
{ patcher::init();
overlay::windows::PatchManager patch_manager(nullptr, false);
}
// load game // load game
avs::game::load_dll(); avs::game::load_dll();
@@ -2570,9 +2652,7 @@ int main_implementation(int argc, char *argv[]) {
} }
// apply patches // apply patches
{ patcher::apply_patches_on_start();
overlay::windows::PatchManager patch_manager(nullptr, true);
}
// load AVS-EA3 // load AVS-EA3
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart); avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
+222 -32
View File
@@ -52,6 +52,7 @@ static const std::vector<std::string> CATEGORY_ORDER_NETWORK = {
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = { static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
"General Overlay", "General Overlay",
"Game Overlay", "Game Overlay",
"OBS Control",
}; };
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = { static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
@@ -433,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Advanced Network", .category = "Advanced Network",
}, },
{ {
.title = "Preferred NetAdapter IP", .title = "Preferred Network Adapter's IP",
.name = "network", .name = "network",
.desc = "This is NOT the EA service URL; use -url for that. " .display_name = "netadapterip",
"Force the use of an adapter with the specified network. Must also provide -subnet.", .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, .type = OptionType::Text,
.category = "Advanced Network", .category = "Advanced Network",
.sensitive = true, .sensitive = true,
}, },
{ {
.title = "Preferred NetAdapter Subnet", .title = "Preferred Network Adapter's Subnet",
.name = "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, .type = OptionType::Text,
.category = "Advanced Network", .category = "Advanced Network",
}, },
@@ -504,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "General Overlay", .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 // OverlayScaling
.title = "Spice Overlay UI Scale %", .title = "Spice Overlay UI Scale %",
@@ -949,13 +962,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Cab Peripherals", .category = "Cab Peripherals",
}, },
{ {
.title = "SDVX FS Subscreen Native Touch Handling", .title = "SDVX Native Touch (DEPRECATED - no longer needed)",
.name = "sdvxnativetouch", .name = "sdvxnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. " .desc = "This option does nothing.\n\n"
"Requires a touch screen to be connected as a secondary monitor. " "Native touch handling is now enabled by default and this option is no longer needed.",
"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.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true,
.game_name = "Sound Voltex", .game_name = "Sound Voltex",
.category = "Advanced Game Options", .category = "Advanced Game Options",
}, },
@@ -1025,6 +1037,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options", .category = "Game Options",
.quick_setting_category = "Game", .quick_setting_category = "Game",
}, },
{
// SDVXDisableLive2D
.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: 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", "Hide Live2D"},
{"ingame", "Hide during songs"},
},
.quick_setting_category = "Game",
},
{ {
// spice2x_SDVXSubPos // spice2x_SDVXSubPos
.title = "SDVX Subscreen Overlay Position", .title = "SDVX Subscreen Overlay Position",
@@ -1128,15 +1158,25 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// PopnNativeTouch // PopnNativeTouch
.title = "Pop'n Music PikaPika Native Touch Handling", .title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
.name = "popnnativetouch", .name = "popnnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. " .desc = "This option does nothing.\n\n"
"Requires a touch screen to be connected as a secondary monitor. " "Native touch handling is now enabled by default and this option is no longer needed.",
"Touch input must be routed to the primary screen via Windows Tablet PC settings. " .type = OptionType::Bool,
"Enable this when you get duplicate touch inputs from an actual touch screen.", .hidden = true,
.game_name = "Pop'n Music",
.category = "Advanced Game Options",
},
{
// PopnSubRedraw
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
.name = "popnsubredraw",
.desc = "Check if submonitor in fullscreen mode appears stuck; "
"this option forces subscreen to redraw every frame.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Pop'n Music", .game_name = "Pop'n Music",
.category = "Advanced Game Options", .category = "Advanced Game Options",
.quick_setting_category = "Game",
}, },
{ {
.title = "Force Load HELLO! Pop'n Music Module", .title = "Force Load HELLO! Pop'n Music Module",
@@ -1742,11 +1782,11 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "I/O Modules", .category = "I/O Modules",
}, },
{ {
.title = "Disable Raw Input Touch", .title = "Touch Compatibility Mode (Disable Raw Input Touch)",
.name = "wintouch", .name = "wintouch",
.desc = "For touch screen input, disable usage of Raw Input API and instead use " .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 " "Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
"using the default (raw input) touch input, as Raw Input performs better.", "touch screens, but may result in worse performance and higher latency.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
@@ -1766,6 +1806,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .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 // DisableTouchCardInsert
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)", .title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
@@ -1777,12 +1835,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_TouchCardInsert // spice2x_TouchCardInsert
.title = "Show Insert Card button", .title = "Show Insert Card button (DEPRECATED)",
.name = "sp2x-touchcard", .name = "sp2x-touchcard",
.display_name = "touchcard", .display_name = "touchcard",
.aliases= "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, .type = OptionType::Bool,
.hidden = true,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
{ {
@@ -1854,7 +1914,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "HID SmartCard Fix UID", .title = "HID SmartCard Fix UID",
.name = "scardfix", .name = "scardfix",
.desc = "Modify behavior of SmartCard UID logic.\n\n" .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" "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" "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. " "The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
@@ -2582,6 +2642,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Windowed Settings", .category = "Game Windowed Settings",
.picker = OptionPickerType::Monitor, .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 // spice2x_IIDXWindowedSubscreenSize
.title = "IIDX Windowed Subscreen Size", .title = "IIDX Windowed Subscreen Size",
@@ -2638,6 +2718,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "For touch screen players: choose the touch algorithm to use.\n\n" .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" "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" "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)", "accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
.type = OptionType::Enum, .type = OptionType::Enum,
.game_name = "Jubeat", .game_name = "Jubeat",
@@ -2645,10 +2726,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.elements = { .elements = {
{"legacy", ""}, {"legacy", ""},
{"improved", ""}, {"improved", ""},
{"plus", ""},
{"accurate", ""}, {"accurate", ""},
}, },
.quick_setting_category = "Game", .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 // spice2x_RBTouchScale
.title = "RB Touch Emulation Scale", .title = "RB Touch Emulation Scale",
@@ -2663,14 +2792,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// RBTouchSize // RBTouchSize
.title = "RB Touch Emulation Size", .title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
.name = "rbtouchsize", .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, .type = OptionType::Enum,
.hidden = true,
.game_name = "Reflec Beat", .game_name = "Reflec Beat",
.category = "Game Options", .category = "Game Options",
.elements = { .elements = {
{"1", "1x1"},
{"3", "3x3"}, {"3", "3x3"},
}, },
}, },
@@ -2678,9 +2808,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// RBTouchPollRate // RBTouchPollRate
.title = "RB Touch Emulation Poll Hz", .title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz", .name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. " .desc = "By default, the game polls for touch at ~125Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n" "This option overrides that rate; enter a number between 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.", "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, .type = OptionType::Integer,
.setting_name = "250", .setting_name = "250",
.game_name = "Reflec Beat", .game_name = "Reflec Beat",
@@ -2757,15 +2888,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_IIDXNativeTouch // spice2x_IIDXNativeTouch
.title = "IIDX TDJ Subscreen Native Touch Handling", .title = "IIDX Native Touch (DEPRECATED - no longer needed)",
.name = "sp2x-iidxnativetouch", .name = "sp2x-iidxnativetouch",
.display_name = "iidxnativetouch", .display_name = "iidxnativetouch",
.aliases= "iidxnativetouch", .aliases= "iidxnativetouch",
.desc = "Disables touch hooks and lets the game access a touch screen directly. " .desc = "This option does nothing.\n\n"
"Requires a touch screen to be connected as a secondary monitor. " "Native touch handling is now enabled by default and this option is no longer needed.",
"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.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true,
.game_name = "Beatmania IIDX", .game_name = "Beatmania IIDX",
.category = "Advanced Game Options", .category = "Advanced Game Options",
}, },
@@ -3019,6 +3149,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Nostalgia", .game_name = "Nostalgia",
.category = "Game Options", .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 // ForceBackBufferCount
@@ -3190,6 +3335,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Development", .category = "Development",
}, },
{
// OBSWebSocketEnabled
.title = "OBS WebSocket Enable",
.name = "obsenable",
.desc = "Enables the in-game OBS Control overlay and its connection to the OBS Studio "
"obs-websocket (v5) server.",
.type = OptionType::Bool,
.category = "OBS Control",
},
{
// OBSWebSocketHost
.title = "OBS WebSocket Host",
.name = "obshost",
.desc = "Host name or IP address of the OBS Studio obs-websocket (v5) server used by "
"the in-game OBS Control overlay. Defaults to 127.0.0.1 when left empty.",
.type = OptionType::Text,
.category = "OBS Control",
},
{
// OBSWebSocketPort
.title = "OBS WebSocket Port",
.name = "obsport",
.desc = "Port of the OBS Studio obs-websocket (v5) server. Defaults to 4455 when left empty.",
.type = OptionType::Integer,
.category = "OBS Control",
},
{
// OBSWebSocketPassword
.title = "OBS WebSocket Password",
.name = "obspass",
.desc = "Password for the OBS Studio obs-websocket (v5) server. Leave empty if "
"authentication is disabled in OBS.",
.type = OptionType::Text,
.category = "OBS Control",
.sensitive = true,
},
{
// OBSWebSocketDebug
.title = "OBS WebSocket Debug",
.name = "obsdebug",
.desc = "Writes the OBS WebSocket client's internal connection diagnostics to the log. "
"Only enable this when troubleshooting connection problems.",
.type = OptionType::Bool,
.category = "OBS Control",
},
}; };
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) { const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+16 -1
View File
@@ -53,6 +53,7 @@ namespace launcher {
SOFTID, SOFTID,
VREnable, VREnable,
DisableOverlay, DisableOverlay,
OverlayKeyboardNavigation,
OverlayScaling, OverlayScaling,
NotificationPosition, NotificationPosition,
spice2x_FpsAutoShow, spice2x_FpsAutoShow,
@@ -97,6 +98,7 @@ namespace launcher {
SDVXDigitalKnobSocd, SDVXDigitalKnobSocd,
spice2x_SDVXAsioDriver, spice2x_SDVXAsioDriver,
SDVXAsioTwoChannel, SDVXAsioTwoChannel,
SDVXDisableLive2D,
spice2x_SDVXSubPos, spice2x_SDVXSubPos,
SDVXSubMonitorOverride, SDVXSubMonitorOverride,
LoadDDRModule, LoadDDRModule,
@@ -108,6 +110,7 @@ namespace launcher {
PopnNoSub, PopnNoSub,
PopnSubMonitorOverride, PopnSubMonitorOverride,
PopnNativeTouch, PopnNativeTouch,
PopnSubRedraw,
LoadHelloPopnMusicModule, LoadHelloPopnMusicModule,
LoadGitaDoraModule, LoadGitaDoraModule,
GitaDoraTwoChannelAudio, GitaDoraTwoChannelAudio,
@@ -177,6 +180,7 @@ namespace launcher {
ForceWinTouch, ForceWinTouch,
ForceTouchEmulation, ForceTouchEmulation,
InvertTouchCoordinates, InvertTouchCoordinates,
RawInputTouchAspectRatio,
DisableTouchCardInsert, DisableTouchCardInsert,
spice2x_TouchCardInsert, spice2x_TouchCardInsert,
ICCAReaderPort, ICCAReaderPort,
@@ -254,12 +258,17 @@ namespace launcher {
GitaDoraWindowedLeftMonitor, GitaDoraWindowedLeftMonitor,
GitaDoraWindowedRightMonitor, GitaDoraWindowedRightMonitor,
GitaDoraWindowedSmallMonitor, GitaDoraWindowedSmallMonitor,
GitaDoraWindowedSmallSize,
GitaDoraWindowedSmallPosition,
spice2x_IIDXWindowedSubscreenSize, spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition, spice2x_IIDXWindowedSubscreenPosition,
IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenBorderless,
IIDXWindowedSubscreenAlwaysOnTop, IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch, spice2x_JubeatLegacyTouch,
JubeatTouchAlgo, JubeatTouchAlgo,
JubeatTouchDebug,
JubeatTouchDebounce,
RBTouchDebug,
spice2x_RBTouchScale, spice2x_RBTouchScale,
RBTouchSize, RBTouchSize,
RBTouchPollRate, RBTouchPollRate,
@@ -288,6 +297,7 @@ namespace launcher {
DDRP4IOBufferMode, DDRP4IOBufferMode,
InputRequiresFocus, InputRequiresFocus,
NostalgiaPoke, NostalgiaPoke,
NostalgiaTouchMode,
ForceBackBufferCount, ForceBackBufferCount,
SDVXWindowedSubscreenSize, SDVXWindowedSubscreenSize,
SDVXWindowedSubscreenPosition, SDVXWindowedSubscreenPosition,
@@ -304,7 +314,12 @@ namespace launcher {
DisableHighResTimer, DisableHighResTimer,
EnableICMPHook, EnableICMPHook,
AutoElevate, AutoElevate,
CfgForceSoftwareRender CfgForceSoftwareRender,
OBSWebSocketEnabled,
OBSWebSocketHost,
OBSWebSocketPort,
OBSWebSocketPassword,
OBSWebSocketDebug
}; };
enum class OptionsCategory { enum class OptionsCategory {
+22
View File
@@ -1327,6 +1327,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
easywsclient (MIT)
-------------------------------------------
Copyright (c) 2012 Dhruv Matani, Daniel Baird
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Contributions Contributions
------------------------------------------- -------------------------------------------
cardio - Felix - MIT License cardio - Felix - MIT License
+34 -32
View File
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
// update touch // update touch
games::jb::touch_update(); games::jb::touch_update();
auto touched = games::jb::touch_state();
// get buttons // get buttons
auto &buttons = games::jb::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))) { if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
DEVICE_INPUT_STATE |= 1 << 29; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; DEVICE_INPUT_STATE |= 1 << 20;
} }
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
// update touch // update touch
games::jb::touch_update(); games::jb::touch_update();
auto touched = games::jb::touch_state();
// get buttons // get buttons
auto &buttons = games::jb::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))) { if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
DEVICE_INPUT_STATE |= 1 << 24; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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 HINSTANCE SCIUNIT_INSTANCE;
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll"; static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
static bool SCIUNIT_INITIALIZED = false; static bool SCIUNIT_INITIALIZED = false;
static int SCIUNIT_STATUS = 0;
static bool CARD_IN = false; static bool CARD_IN = false;
static bool CARD_PRESSED = false; static bool CARD_PRESSED = false;
static uint8_t CARD_UID[8]; static uint8_t CARD_UID[8];
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
} }
static int __cdecl sciunit_get_errorunit() { static int __cdecl sciunit_get_errorunit() {
return 0; return -1;
} }
static int __cdecl sciunit_get_stat() { static int __cdecl sciunit_get_stat() {
return 0; return SCIUNIT_STATUS;
} }
static int __cdecl sciunit_get_version(int a1, int a2) { 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() { static int __cdecl sciunit_initialize() {
SCIUNIT_INITIALIZED = true; SCIUNIT_INITIALIZED = true;
SCIUNIT_STATUS = 1;
return 0; return 0;
} }
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
} }
static int __cdecl sciunit_update() { static int __cdecl sciunit_update() {
if (SCIUNIT_INITIALIZED) if (SCIUNIT_INITIALIZED) {
if (SCIUNIT_STATUS == 1) {
SCIUNIT_STATUS = 2;
}
update_card(); update_card();
}
return 0; return 0;
} }
+11 -236
View File
@@ -4,44 +4,19 @@
#include "wintouchemu.h" #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 "hooks/graphics/graphics.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
namespace wintouchemu { namespace wintouchemu {
typedef struct {
POINT pos;
bool last_button_pressed;
DWORD touch_event;
} mouse_state_t;
// settings // settings
bool FORCE = false; bool FORCE = false;
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false; bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() { static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR; return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
} }
@@ -81,16 +56,13 @@ namespace wintouchemu {
// state // state
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int); BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
bool USE_MOUSE = false;
std::vector<TouchEvent> TOUCH_EVENTS; std::vector<TouchEvent> TOUCH_EVENTS;
std::vector<TouchPoint> TOUCH_POINTS; std::vector<TouchPoint> TOUCH_POINTS;
HMODULE HOOKED_MODULE = nullptr; HMODULE HOOKED_MODULE = nullptr;
std::string WINDOW_TITLE_START = ""; std::string WINDOW_TITLE_START = "";
std::optional<std::string> WINDOW_TITLE_END = std::nullopt; bool INITIALIZED = false;
volatile bool INITIALIZED = false;
mouse_state_t mouse_state;
void hook(const char *window_title, HMODULE module, int delay_in_s) { void hook(const char *window_title, HMODULE module) {
// hooks // hooks
auto system_metrics_hook = detour::iat_try( auto system_metrics_hook = detour::iat_try(
@@ -109,31 +81,8 @@ namespace wintouchemu {
// set module and title // set module and title
HOOKED_MODULE = module; HOOKED_MODULE = module;
WINDOW_TITLE_START = window_title; WINDOW_TITLE_START = window_title;
log_misc("wintouchemu", "initializing");
if (0 < delay_in_s) { INITIALIZED = true;
// some games crash when touch events are injected too early during boot
std::thread t([&]() {
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
log_misc("wintouchemu", "initializing", delay_in_s);
INITIALIZED = true;
});
t.detach();
} else {
log_misc("wintouchemu", "initializing");
INITIALIZED = true;
}
}
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
hook(window_title_start, module);
WINDOW_TITLE_END = window_title_end;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
} }
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
@@ -145,7 +94,6 @@ namespace wintouchemu {
// set touch inputs // set touch inputs
bool result = false; bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) { for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input]; auto *touch_input = &pInputs[input];
@@ -178,13 +126,7 @@ namespace wintouchemu {
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y)); // log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
if (GRAPHICS_IIDX_WSUB) { if (overlay::OVERLAY) {
// touch was received on subscreen window.
RECT clientRect {};
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
} else if (overlay::OVERLAY) {
// touch was received on global coords // touch was received on global coords
valid = overlay::OVERLAY->transform_touch_point(&x, &y); valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else { } else {
@@ -230,89 +172,10 @@ namespace wintouchemu {
touch_input->cxContact = 0; touch_input->cxContact = 0;
touch_input->cyContact = 0; touch_input->cyContact = 0;
if (avs::game::is_model("KFC") && } else {
rawinput::touch::DISPLAY_INITIALIZED &&
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_touch_points(touch_input);
}
} else if (USE_MOUSE && !mouse_used) {
// disable further mouse inputs this call
mouse_used = true;
if (mouse_state.touch_event) {
result = true;
touch_input->x = mouse_state.pos.x;
touch_input->y = mouse_state.pos.y;
if (GRAPHICS_WINDOWED) {
touch_input->x -= SPICETOUCH_TOUCH_X;
touch_input->y -= SPICETOUCH_TOUCH_Y;
}
// log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true;
if (overlay::OVERLAY) {
valid = overlay::OVERLAY->transform_touch_point(
&touch_input->x, &touch_input->y);
}
// touch inputs require 100x precision per pixel
touch_input->x *= 100;
touch_input->y *= 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = 0;
touch_input->dwFlags = 0;
switch (mouse_state.touch_event) {
case TOUCHEVENTF_DOWN:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
}
break;
case TOUCHEVENTF_MOVE:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
}
break;
case TOUCHEVENTF_UP:
// don't check valid so that this touch ID can be released
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_UP;
break;
}
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
// reset it since the event was consumed & propagated as touch
mouse_state.touch_event = 0;
}
} else if (!GRAPHICS_IIDX_WSUB) {
/*
* For some reason, Nostalgia won't show an active touch point unless a move event
* triggers in the same frame. To work around this, we just supply a fake move
* event if we didn't update the same pointer ID in the same call.
*/
// beatstream requires a MOVE for active points in each update
// add one for every active point without a matching input event
// find touch point which has no associated input event // find touch point which has no associated input event
TouchPoint *touch_point = nullptr; TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -380,21 +243,6 @@ namespace wintouchemu {
title = get_window_title(hWnd); 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 // check window
if (hWnd == nullptr) { if (hWnd == nullptr) {
return; return;
@@ -402,39 +250,13 @@ namespace wintouchemu {
// check if windowed // check if windowed
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
if (GRAPHICS_IIDX_WSUB) { // create touch window - create overlay if not yet existing at this point
// no handling is needed here log_info("wintouchemu", "create touch window relative to main game window");
// graphics::MoveWindow_hook will attach hook to windowed subscreen touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
USE_MOUSE = false;
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
// overlay subscreen in IIDX
// use mouse position as ImGui overlay will block the touch window
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
USE_MOUSE = true;
} else if (games::popn::is_pikapika_model()) {
// same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true;
} else {
// create touch window - create overlay if not yet existing at this point
log_info("wintouchemu", "create touch window relative to main game window");
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
USE_MOUSE = false;
}
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
log_info(
"wintouchemu",
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
USE_MOUSE = true;
} else { } else {
log_info("wintouchemu", "activating DirectX hooks"); log_info("wintouchemu", "activating DirectX hooks");
// mouse position based input only // mouse position based input only
touch_attach_dx_hook(); touch_attach_dx_hook();
USE_MOUSE = false;
} }
// hooks // hooks
@@ -445,8 +267,6 @@ namespace wintouchemu {
} }
} }
const auto now = get_performance_milliseconds();
// update touch events // update touch events
if (hWnd != nullptr) { if (hWnd != nullptr) {
@@ -466,56 +286,11 @@ namespace wintouchemu {
// check if new events are available // check if new events are available
if (event_count > 0) { if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs // send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC); auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook); 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 // settings
extern bool FORCE; extern bool FORCE;
extern bool INJECT_MOUSE_AS_WM_TOUCH;
extern bool LOG_FPS;
extern bool ADD_TOUCH_FLAG_PRIMARY; extern bool ADD_TOUCH_FLAG_PRIMARY;
void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0); void hook(const char *window_title, HMODULE module = nullptr);
void hook_title_ends(
const char *window_title_start,
const char *window_title_ends,
HMODULE module = nullptr);
void update(); void update();
} }
+26 -11
View File
@@ -1,7 +1,9 @@
#include "extensions.h" #include "extensions.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#include "external/imgui/imgui_internal.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
@@ -160,6 +162,19 @@ namespace ImGui {
return clicked; return clicked;
} }
bool ColoredButton(const char* label, const ImVec4& base, const ImVec2& size) {
const auto brighten = [](const ImVec4& c, float d) {
return ImVec4((std::min)(c.x + d, 1.0f), (std::min)(c.y + d, 1.0f),
(std::min)(c.z + d, 1.0f), c.w);
};
ImGui::PushStyleColor(ImGuiCol_Button, base);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(base, 0.12f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(base, 0.22f));
const bool clicked = ImGui::Button(label, size);
ImGui::PopStyleColor(3);
return clicked;
}
bool ClearButton(const std::string& tooltip) { bool ClearButton(const std::string& tooltip) {
ImGui::PushID(tooltip.c_str()); ImGui::PushID(tooltip.c_str());
// same colors as a checkbox // same colors as a checkbox
@@ -209,17 +224,17 @@ namespace ImGui {
return open; return open;
} }
void InvisibleTableRowSelectable() { void HighlightTableRowOnHover() {
ImGui::TableSetColumnIndex(0); // hit-test the row rect directly so the row layout and height are untouched
ImGui::PushStyleColor(ImGuiCol_Header, 0); ImGuiTable *table = ImGui::GetCurrentTable();
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, 0); if (table == nullptr) {
ImGui::PushStyleColor(ImGuiCol_HeaderActive, 0); return;
ImGui::PushTabStop(false); // prevent tab navigation }
ImGui::Selectable("##row", false, if (ImGui::IsWindowHovered() &&
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap); ImGui::IsMouseHoveringRect(
ImGui::PopTabStop(); ImVec2(table->WorkRect.Min.x, table->RowPosY1),
ImGui::PopStyleColor(3); ImVec2(table->WorkRect.Max.x, table->RowPosY2),
if (ImGui::IsItemHovered()) { false)) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24)); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
} }
} }
+6 -1
View File
@@ -20,7 +20,12 @@ namespace ImGui {
void TextTruncated(const std::string& p_text, float p_truncated_width); void TextTruncated(const std::string& p_text, float p_truncated_width);
bool DeleteButton(const std::string& tooltip); bool DeleteButton(const std::string& tooltip);
bool ClearButton(const std::string& tooltip); bool ClearButton(const std::string& tooltip);
void InvisibleTableRowSelectable(); void HighlightTableRowOnHover();
// a Button with the given base fill color; the hovered/active shades are
// derived by brightening the base. size defaults to auto (fit the label).
bool ColoredButton(const char* label, const ImVec4& base,
const ImVec2& size = ImVec2(0, 0));
// Config tab bar with extra label padding and uniform, centered tab widths. // Config tab bar with extra label padding and uniform, centered tab widths.
// Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar. // Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar.
+15 -3
View File
@@ -210,7 +210,15 @@ bool ImGui_ImplSpice_UpdateMouseCursor() {
// update cursor // update cursor
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) {
// in auto-hide mode imgui owns cursor drawing, so the OS cursor must stay
// hidden. this function is also called from the game window's WM_SETCURSOR
// handler, which fires on every mouse move; without forcing it hidden here,
// the else branch below would set IDC_ARROW on the next mouse move once the
// overlay is closed. that arrow is conspicuous on games whose window class
// has hCursor==NULL (e.g. DDR), since those normally show no client-area
// cursor at all.
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor || g_MouseCursorAutoHide) {
// hide OS mouse cursor if imgui is drawing it or if it wants no cursor // hide OS mouse cursor if imgui is drawing it or if it wants no cursor
::SetCursor(nullptr); ::SetCursor(nullptr);
@@ -592,8 +600,12 @@ void ImGui_ImplSpice_NewFrame() {
} }
} }
} else { } else {
// update OS mouse cursor with the cursor requested by imgui // in auto-hide mode imgui owns the cursor, so keep the OS cursor hidden
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor(); // even when the overlay is closed (io.MouseDrawCursor is false then).
ImGuiMouseCursor mouse_cursor =
(io.MouseDrawCursor || g_MouseCursorAutoHide)
? ImGuiMouseCursor_None
: ImGui::GetMouseCursor();
if (g_LastMouseCursor != mouse_cursor) { if (g_LastMouseCursor != mouse_cursor) {
g_LastMouseCursor = mouse_cursor; g_LastMouseCursor = mouse_cursor;
ImGui_ImplSpice_UpdateMouseCursor(); ImGui_ImplSpice_UpdateMouseCursor();
+45 -1
View File
@@ -5,7 +5,9 @@
#include "games/io.h" #include "games/io.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/nost/nost.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "games/rb/touch_debug.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/touch.h" #include "touch/touch.h"
@@ -50,6 +52,8 @@
#include "windows/sdvx_sub.h" #include "windows/sdvx_sub.h"
#include "windows/keypad.h" #include "windows/keypad.h"
#include "windows/log.h" #include "windows/log.h"
#include "windows/nostalgia_touch_piano.h"
#include "windows/obs.h"
#include "windows/patch_manager.h" #include "windows/patch_manager.h"
#include "windows/exitprompt.cpp" #include "windows/exitprompt.cpp"
@@ -65,6 +69,7 @@ namespace overlay {
bool AUTO_SHOW_IOPANEL = false; bool AUTO_SHOW_IOPANEL = false;
bool AUTO_SHOW_KEYPAD_P1 = false; bool AUTO_SHOW_KEYPAD_P1 = false;
bool AUTO_SHOW_KEYPAD_P2 = false; bool AUTO_SHOW_KEYPAD_P2 = false;
bool ENABLE_KEYBOARD_NAVIGATION = false;
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false; bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
FpsLocation FPS_LOCATION = FpsLocation::TopRight; FpsLocation FPS_LOCATION = FpsLocation::TopRight;
std::optional<uint32_t> UI_SCALE_PERCENT; std::optional<uint32_t> UI_SCALE_PERCENT;
@@ -310,6 +315,9 @@ void overlay::SpiceOverlay::init() {
auto &io = ImGui::GetIO(); auto &io = ImGui::GetIO();
io.UserData = this; io.UserData = this;
io.ConfigFlags = ImGuiConfigFlags_None; io.ConfigFlags = ImGuiConfigFlags_None;
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
}
if (!cfg::CONFIGURATOR_STANDALONE) { if (!cfg::CONFIGURATOR_STANDALONE) {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
} }
@@ -398,6 +406,12 @@ void overlay::SpiceOverlay::init() {
window_fps->set_active(true); 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 // owned separately from `windows` so it is not part of the overlay window layer
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this); window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
@@ -416,6 +430,12 @@ void overlay::SpiceOverlay::init() {
} }
this->window_add(new overlay::windows::PatchManager(this)); this->window_add(new overlay::windows::PatchManager(this));
// OBS control spawns a background WebSocket worker; skip it in the standalone
// configurator, where there is no running game to stream/record
if (!cfg::CONFIGURATOR_STANDALONE) {
this->window_add(window_obs = new overlay::windows::OBSControl(this));
}
{ {
window_keypad1 = new overlay::windows::Keypad(this, 0); window_keypad1 = new overlay::windows::Keypad(this, 0);
this->window_add(window_keypad1); this->window_add(window_keypad1);
@@ -544,9 +564,18 @@ void overlay::SpiceOverlay::new_frame() {
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
&& this->window_fps->get_active(); && 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 // check if there is nothing to draw
this->has_pending_frame = false; 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; return;
} }
@@ -571,12 +600,22 @@ void overlay::SpiceOverlay::new_frame() {
ImGui::NewFrame(); ImGui::NewFrame();
this->has_pending_frame = true; 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 // build windows only when the overlay itself is active
if (this->active) { if (this->active) {
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->build(); 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 // draw the main menu on top of the overlay windows
this->window_main_menu->build(); this->window_main_menu->build();
@@ -588,6 +627,7 @@ void overlay::SpiceOverlay::new_frame() {
if (draw_fps_persistent) { if (draw_fps_persistent) {
this->window_fps->build(); this->window_fps->build();
} }
// draw notifications last so they paint on top of any overlay windows // draw notifications last so they paint on top of any overlay windows
if (draw_notifications) { if (draw_notifications) {
overlay::notifications::draw(); overlay::notifications::draw();
@@ -798,6 +838,10 @@ void overlay::SpiceOverlay::update() {
// FPS window // FPS window
this->window_fps->update(); 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) // main menu (owned separately from the overlay window layer)
this->window_main_menu->update(); this->window_main_menu->update();
+6
View File
@@ -44,6 +44,7 @@ namespace overlay {
extern bool AUTO_SHOW_IOPANEL; extern bool AUTO_SHOW_IOPANEL;
extern bool AUTO_SHOW_KEYPAD_P1; extern bool AUTO_SHOW_KEYPAD_P1;
extern bool AUTO_SHOW_KEYPAD_P2; extern bool AUTO_SHOW_KEYPAD_P2;
extern bool ENABLE_KEYBOARD_NAVIGATION;
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT; extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
extern FpsLocation FPS_LOCATION; extern FpsLocation FPS_LOCATION;
extern bool SHOW_DEBUG_LOG_WINDOW; extern bool SHOW_DEBUG_LOG_WINDOW;
@@ -77,10 +78,12 @@ namespace overlay {
Window *window_camera = nullptr; Window *window_camera = nullptr;
Window *window_sub = nullptr; Window *window_sub = nullptr;
Window *window_log = nullptr; Window *window_log = nullptr;
Window *window_obs = nullptr;
// not part of `windows`: drawn/updated on the persistent layer (like // not part of `windows`: drawn/updated on the persistent layer (like
// notifications), independent of the overlay's active state and input gates. // notifications), independent of the overlay's active state and input gates.
std::unique_ptr<Window> window_fps; 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 // not part of `windows`: the main menu / launcher. owned and drawn
// separately from the overlay window layer; it drives the overlay's // separately from the overlay window layer; it drives the overlay's
@@ -136,6 +139,9 @@ namespace overlay {
inline IDirect3DDevice9 *get_device() { inline IDirect3DDevice9 *get_device() {
return this->device; return this->device;
} }
inline HWND get_window() {
return this->hWnd;
}
bool can_transform_touch_input() { bool can_transform_touch_input() {
return (this->subscreen_mouse_handler != nullptr); return (this->subscreen_mouse_handler != nullptr);
+208 -68
View File
@@ -85,6 +85,7 @@ namespace overlay::windows {
static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = { static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = {
{ "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS }, { "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS },
{ "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS }, { "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS },
{ "Modifiers", ControllerPage::CONTROLLER_PAGE_MODIFIERS },
{ "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS }, { "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS },
{ "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY }, { "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY },
{ "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS }, { "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS },
@@ -251,7 +252,7 @@ namespace overlay::windows {
} }
// Renders an arrow-less, non-collapsible left-nav header row, highlighted when // 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. // own selection side effects.
bool Config::build_nav_header(const char *label, bool active) { bool Config::build_nav_header(const char *label, bool active) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf; ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf;
@@ -282,7 +283,7 @@ namespace overlay::windows {
ImGui::PopStyleColor(colors_pushed); ImGui::PopStyleColor(colors_pushed);
} }
return ImGui::IsItemClicked(); return ImGui::IsItemActivated();
} }
void Config::build_options_tab(float page_offset) { void Config::build_options_tab(float page_offset) {
@@ -616,6 +617,21 @@ namespace overlay::windows {
} }
break; 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: { case ControllerPage::CONTROLLER_PAGE_ANALOGS: {
// help text for binding analog, if the game has one // help text for binding analog, if the game has one
@@ -928,18 +944,14 @@ namespace overlay::windows {
// primary // primary
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0); build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
ImGui::PushID(&primary_button); ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
// alternatives // alternatives
int alt_index = 1; // 0 is primary int alt_index = 1; // 0 is primary
for (auto &alt : primary_button.getAlternatives()) { for (auto &alt : primary_button.getAlternatives()) {
if (alt.isValid()) { if (alt.isValid()) {
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index); build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
ImGui::PushID(&alt); ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
} }
alt_index++; alt_index++;
} }
@@ -1260,7 +1272,8 @@ namespace overlay::windows {
if (open_edit) { if (open_edit) {
ImGui::OpenPopup(edit_name.c_str()); 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 // clean up
ImGui::PopID(); ImGui::PopID();
@@ -1279,6 +1292,7 @@ namespace overlay::windows {
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setInvert(false); button->setInvert(false);
button->setModifierMask(0);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED); button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
button->setLastVelocity(0); button->setLastVelocity(0);
button->setTemporary(false); button->setTemporary(false);
@@ -1360,8 +1374,9 @@ namespace overlay::windows {
if (check_devices) { if (check_devices) {
// iterate updated devices // iterate updated devices
auto updated_devices = RI_MGR->devices_get_updated(); auto updated_devices = RI_MGR->devices_get_updated();
bool binding_finished = false;
for (auto device : updated_devices) { for (auto device : updated_devices) {
std::lock_guard<std::mutex> lock(*device->mutex); std::unique_lock<std::mutex> lock(*device->mutex);
switch (device->type) { switch (device->type) {
case rawinput::MOUSE: { case rawinput::MOUSE: {
auto mouse = device->mouseInfo; auto mouse = device->mouseInfo;
@@ -1373,12 +1388,13 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name); button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i)); button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1); games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} }
@@ -1397,7 +1413,7 @@ namespace overlay::windows {
buttons_bind_active = false; buttons_bind_active = false;
buttons_many_index = -1; buttons_many_index = -1;
buttons_many_active = false; buttons_many_active = false;
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
@@ -1405,12 +1421,13 @@ namespace overlay::windows {
button->setDeviceIdentifier(device->name); button->setDeviceIdentifier(device->name);
button->setVKey(vkey); button->setVKey(vkey);
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1); games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} }
@@ -1540,13 +1557,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
} }
break; break;
@@ -1585,6 +1603,7 @@ namespace overlay::windows {
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setModifierMask(0);
// same idea as setMidiVKey - keep velocity threshold consistent // same idea as setMidiVKey - keep velocity threshold consistent
button->setVelocityThreshold( button->setVelocityThreshold(
device->midiInfo->v2_velocity_threshold[button->getVKey()]); device->midiInfo->v2_velocity_threshold[button->getVKey()]);
@@ -1594,7 +1613,7 @@ namespace overlay::windows {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} }
@@ -1612,13 +1631,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} else { } else {
@@ -1639,13 +1659,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} else { } else {
@@ -1666,13 +1687,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} else { } else {
@@ -1692,13 +1714,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
@@ -1713,13 +1736,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} }
@@ -1743,13 +1767,14 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); binding_finished = true;
break; break;
} }
} }
@@ -1759,6 +1784,13 @@ namespace overlay::windows {
default: default:
break; break;
} }
lock.unlock();
if (binding_finished) {
break;
}
}
if (binding_finished) {
RI_MGR->devices_midi_freeze(false);
} }
} }
@@ -1812,6 +1844,7 @@ namespace overlay::windows {
if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) { if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) {
button->setDeviceIdentifier(xinput::get_device_desc(xinput.player)); button->setDeviceIdentifier(xinput::get_device_desc(xinput.player));
button->setVKey(static_cast<uint16_t>(xinput.button)); button->setVKey(static_cast<uint16_t>(xinput.button));
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, alt_index - 1); games_list[games_selected], *button, alt_index - 1);
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1868,6 +1901,7 @@ namespace overlay::windows {
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0); button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setModifierMask(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
alt_index - 1); alt_index - 1);
@@ -1886,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( void Config::edit_button_popup(
const std::string &edit_name, const std::string &edit_name,
const std::string &button_display, const std::string &button_display,
Button *button, Button *button,
const float button_velocity, 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); const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
bool dirty = false; bool dirty = false;
auto device = RI_MGR->devices_get(button->getDeviceIdentifier()); 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 // binding
ImGui::Text("Binding"); ImGui::Text("Binding");
ImGui::Separator();
// combo for devices // combo for devices
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)"; std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) { if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) { if (ImGui::Selectable("Empty (Naive)", button->isNaive()) && !button->isNaive()) {
button->setDeviceIdentifier(""); switch_device("");
dirty = true;
} }
if (button->isNaive()) { if (button->isNaive()) {
ImGui::SetItemDefaultFocus(); ImGui::SetItemDefaultFocus();
@@ -1914,17 +2024,8 @@ namespace overlay::windows {
for (auto &device : RI_MGR->devices_get()) { for (auto &device : RI_MGR->devices_get()) {
bool selected = button->getDeviceIdentifier() == device.name.c_str(); bool selected = button->getDeviceIdentifier() == device.name.c_str();
const auto device_desc = fmt::format("{}##{}", device.desc, device.name); const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
if (ImGui::Selectable(device_desc.c_str(), selected)) { if (ImGui::Selectable(device_desc.c_str(), selected) && !selected) {
button->setDeviceIdentifier(device.name); switch_device(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 (selected) { if (selected) {
ImGui::SetItemDefaultFocus(); ImGui::SetItemDefaultFocus();
@@ -1932,6 +2033,7 @@ namespace overlay::windows {
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
device = RI_MGR->devices_get(button->getDeviceIdentifier());
// analog type (only for HID) // analog type (only for HID)
const auto bat = button->getAnalogType(); const auto bat = button->getAnalogType();
@@ -2223,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 // preview
if (!button_display.empty()) { if (!button_display.empty()) {
ImGui::TextUnformatted("\nPreview"); ImGui::TextUnformatted("\nPreview");
ImGui::Separator(); ImGui::Separator();
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) { if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f)); ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
// 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::TextTruncated(button_display, ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted("\n"); ImGui::TextUnformatted("\n");
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) { if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -2246,8 +2349,10 @@ namespace overlay::windows {
ImGui::TextUnformatted(""); ImGui::TextUnformatted("");
} }
// options // options
ImGui::Text("Options"); ImGui::TextUnformatted("Options");
ImGui::Separator();
// check for debounce // check for debounce
if (button->getDebounceUp() || button->getDebounceDown() if (button->getDebounceUp() || button->getDebounceDown()
@@ -2366,6 +2471,7 @@ namespace overlay::windows {
ImGui::ProgressBar(button_velocity); ImGui::ProgressBar(button_velocity);
} else { } else {
ImGui::Text("State"); ImGui::Text("State");
ImGui::Separator();
ImGui::ProgressBar(button_velocity); ImGui::ProgressBar(button_velocity);
} }
@@ -2519,8 +2625,7 @@ namespace overlay::windows {
edit_analog_popup(analog, title); edit_analog_popup(analog, title);
// row hover detection (invisible selectable that spans entire row) ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
// clean up // clean up
ImGui::PopID(); ImGui::PopID();
@@ -3174,16 +3279,12 @@ namespace overlay::windows {
} }
build_light(light, &light, i, 0); build_light(light, &light, i, 0);
ImGui::PushID(&light); ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
int alt_index = 1; int alt_index = 1;
for (auto &alt : light.getAlternatives()) { for (auto &alt : light.getAlternatives()) {
if (alt.isValid()) { if (alt.isValid()) {
build_light(light, &alt, i, alt_index); build_light(light, &alt, i, alt_index);
ImGui::PushID(&alt); ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
} }
alt_index++; alt_index++;
} }
@@ -4209,8 +4310,10 @@ namespace overlay::windows {
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides"); ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextUnformatted( ImGui::TextWrapped(
"Specify hardcoded card numbers here. This will always take priority when Insert Card is pressed."); "%s",
"Specify hardcoded card numbers here.\n\n"
"Overrides will always take priority when Insert Card is pressed.");
ImGui::Spacing(); ImGui::Spacing();
// read in values from options // read in values from options
@@ -4387,7 +4490,8 @@ namespace overlay::windows {
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files"); ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files");
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextUnformatted( ImGui::TextWrapped(
"%s",
"Use text files on disk; its content will be read when Insert Card is pressed."); "Use text files on disk; its content will be read when Insert Card is pressed.");
ImGui::Spacing(); ImGui::Spacing();
@@ -5293,8 +5397,7 @@ namespace overlay::windows {
} }
} }
// row hover detection (invisible selectable that spans entire row) ImGui::HighlightTableRowOnHover();
ImGui::InvisibleTableRowSelectable();
// next item // next item
ImGui::PopID(); ImGui::PopID();
@@ -5489,6 +5592,12 @@ namespace overlay::windows {
tmpl.buttons.emplace_back(btn); 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 // capture keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name); auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
@@ -5544,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 // clear all keypad buttons
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name); auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
if (keypad_buttons) { if (keypad_buttons) {
@@ -5637,6 +5757,7 @@ namespace overlay::windows {
btn.setDebounceDown(entry->debounce_down); btn.setDebounceDown(entry->debounce_down);
btn.setBatThreshold(entry->bat_threshold); btn.setBatThreshold(entry->bat_threshold);
btn.setVelocityThreshold(entry->velocity_threshold); btn.setVelocityThreshold(entry->velocity_threshold);
btn.setModifierMask(entry->modifier_mask);
::Config::getInstance().updateBinding(game, btn, -1); ::Config::getInstance().updateBinding(game, btn, -1);
} else { } else {
Button alt_btn(btn.getName()); Button alt_btn(btn.getName());
@@ -5648,6 +5769,7 @@ namespace overlay::windows {
alt_btn.setDebounceDown(entry->debounce_down); alt_btn.setDebounceDown(entry->debounce_down);
alt_btn.setBatThreshold(entry->bat_threshold); alt_btn.setBatThreshold(entry->bat_threshold);
alt_btn.setVelocityThreshold(entry->velocity_threshold); alt_btn.setVelocityThreshold(entry->velocity_threshold);
alt_btn.setModifierMask(entry->modifier_mask);
alt_btn.setTemporary(true); alt_btn.setTemporary(true);
btn.getAlternatives().push_back(alt_btn); btn.getAlternatives().push_back(alt_btn);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
@@ -5666,6 +5788,11 @@ namespace overlay::windows {
if (this->apply_buttons) { if (this->apply_buttons) {
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name)); 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) { if (this->apply_keypads) {
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name)); apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
} }
@@ -5787,23 +5914,28 @@ namespace overlay::windows {
// name // name
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::TextTruncated( ImGui::TextTruncated(
t.name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20)); t.name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
// type // type
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User"); ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User");
// buttons // buttons
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%d", (int)t.buttons.size()); ImGui::Text("%d", (int)t.buttons.size());
// analogs // analogs
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%d", (int)t.analogs.size()); ImGui::Text("%d", (int)t.analogs.size());
// lights // lights
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%d", (int)t.lights.size()); ImGui::Text("%d", (int)t.lights.size());
// actions // actions
@@ -5831,7 +5963,7 @@ namespace overlay::windows {
} }
} }
ImGui::InvisibleTableRowSelectable(); ImGui::HighlightTableRowOnHover();
ImGui::PopID(); ImGui::PopID();
} }
@@ -5902,7 +6034,7 @@ namespace overlay::windows {
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker( ImGui::HelpMarker(
"Clears all game button, analog, and light bindings."); "Clears all game button, modifier, keypad, analog, and light bindings.");
if (this->all_cleared) { if (this->all_cleared) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextUnformatted("Done."); ImGui::TextUnformatted("Done.");
@@ -5917,6 +6049,8 @@ namespace overlay::windows {
bool selection_changed = false; bool selection_changed = false;
selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons); selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons);
ImGui::SameLine(); ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Modifiers", &this->apply_modifiers);
ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads); selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads);
ImGui::SameLine(); ImGui::SameLine();
selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs); selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs);
@@ -5926,7 +6060,7 @@ namespace overlay::windows {
std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false); 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) { !this->apply_analogs && !this->apply_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n"); ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n");
} }
@@ -5966,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(); ImGui::BeginDisabled();
} }
@@ -6135,7 +6270,8 @@ namespace overlay::windows {
ImGui::EndTable(); ImGui::EndTable();
} }
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) { if (!apply_buttons && !apply_modifiers && !apply_keypads &&
!apply_analogs && !apply_lights) {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
} }
@@ -6214,12 +6350,14 @@ namespace overlay::windows {
ImGui::TextUnformatted("Pick which groups to save:"); ImGui::TextUnformatted("Pick which groups to save:");
ImGui::Checkbox("Buttons", &this->save_buttons); ImGui::Checkbox("Buttons", &this->save_buttons);
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Modifiers", &this->save_modifiers);
ImGui::SameLine();
ImGui::Checkbox("Keypads", &this->save_keypads); ImGui::Checkbox("Keypads", &this->save_keypads);
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Analogs", &this->save_analogs); ImGui::Checkbox("Analogs", &this->save_analogs);
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Lights", &this->save_lights); 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) { !this->save_analogs && !this->save_lights) {
ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n"); ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n");
} }
@@ -6271,7 +6409,8 @@ namespace overlay::windows {
ImGui::BeginDisabled( ImGui::BeginDisabled(
!all_labels_set || !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")) { if (ImGui::Button("Save")) {
// replace device IDs with labels in the template // replace device IDs with labels in the template
@@ -6280,7 +6419,8 @@ namespace overlay::windows {
template_save_sources[si], template_save_labels[si]); template_save_sources[si], template_save_labels[si]);
} }
if (save_user_template(template_pending_save, 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'; template_save_name[0] = '\0';
templates_cache_dirty = true; templates_cache_dirty = true;
} }
@@ -6362,7 +6502,7 @@ namespace overlay::windows {
t.rename_source(template_save_sources[si], template_save_labels[si]); 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; templates_cache_dirty = true;
template_save_sources.clear(); template_save_sources.clear();
template_save_labels.clear(); template_save_labels.clear();
+6 -1
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
CONTROLLER_PAGE_INVALID, CONTROLLER_PAGE_INVALID,
CONTROLLER_PAGE_BUTTONS, CONTROLLER_PAGE_BUTTONS,
CONTROLLER_PAGE_KEYPADS, CONTROLLER_PAGE_KEYPADS,
CONTROLLER_PAGE_MODIFIERS,
CONTROLLER_PAGE_ANALOGS, CONTROLLER_PAGE_ANALOGS,
CONTROLLER_PAGE_OVERLAY, CONTROLLER_PAGE_OVERLAY,
CONTROLLER_PAGE_LIGHTS, CONTROLLER_PAGE_LIGHTS,
@@ -119,10 +120,12 @@ namespace overlay::windows {
std::vector<std::string> template_save_sources; std::vector<std::string> template_save_sources;
std::vector<std::string> template_save_labels; std::vector<std::string> template_save_labels;
bool apply_buttons = true; bool apply_buttons = true;
bool apply_modifiers = true;
bool apply_keypads = true; bool apply_keypads = true;
bool apply_analogs = true; bool apply_analogs = true;
bool apply_lights = true; bool apply_lights = true;
bool save_buttons = true; bool save_buttons = true;
bool save_modifiers = true;
bool save_keypads = true; bool save_keypads = true;
bool save_analogs = true; bool save_analogs = true;
bool save_lights = 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 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); 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( void edit_button_popup(
const std::string &edit_name, const std::string &edit_name,
const std::string &button_display, const std::string &button_display,
Button *button, Button *button,
const float button_velocity, 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 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); void reset_button_to_default(Button *button, unsigned short vKey_default);
unsigned int get_keypad_top_row(const Button &button); unsigned int get_keypad_top_row(const Button &button);
@@ -6,7 +6,7 @@
namespace overlay::windows { 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) { static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
for (auto &btn : btns) { for (auto &btn : btns) {
if (btn.primary.is_naive()) naive++; if (btn.primary.is_naive()) naive++;
@@ -21,6 +21,7 @@ namespace overlay::windows {
int ControllerTemplate::count_naive_buttons() const { int ControllerTemplate::count_naive_buttons() const {
int naive = 0, device = 0; int naive = 0, device = 0;
count_buttons_in(buttons, naive, device); count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device); count_buttons_in(keypad_buttons, naive, device);
return naive; return naive;
} }
@@ -28,6 +29,7 @@ namespace overlay::windows {
int ControllerTemplate::count_device_buttons() const { int ControllerTemplate::count_device_buttons() const {
int naive = 0, device = 0; int naive = 0, device = 0;
count_buttons_in(buttons, naive, device); count_buttons_in(buttons, naive, device);
count_buttons_in(modifier_buttons, naive, device);
count_buttons_in(keypad_buttons, naive, device); count_buttons_in(keypad_buttons, naive, device);
return device; return device;
} }
@@ -64,6 +66,7 @@ namespace overlay::windows {
std::set<std::string> ControllerTemplate::get_used_devices() const { std::set<std::string> ControllerTemplate::get_used_devices() const {
std::set<std::string> devices; std::set<std::string> devices;
collect_devices_from(buttons, devices); collect_devices_from(buttons, devices);
collect_devices_from(modifier_buttons, devices);
collect_devices_from(keypad_buttons, devices); collect_devices_from(keypad_buttons, devices);
for (auto &a : analogs) { for (auto &a : analogs) {
if (a.is_device()) devices.insert(a.device_identifier); if (a.is_device()) devices.insert(a.device_identifier);
@@ -94,6 +97,7 @@ namespace overlay::windows {
bool has_naive = false; bool has_naive = false;
collect_sources_from(buttons, sources_set, has_naive); 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); collect_sources_from(keypad_buttons, sources_set, has_naive);
for (auto &a : analogs) { for (auto &a : analogs) {
if (a.is_device()) { if (a.is_device()) {
@@ -131,6 +135,7 @@ namespace overlay::windows {
} }
}; };
rename_in_buttons(buttons); rename_in_buttons(buttons);
rename_in_buttons(modifier_buttons);
rename_in_buttons(keypad_buttons); rename_in_buttons(keypad_buttons);
for (auto &a : analogs) { for (auto &a : analogs) {
if (a.device_identifier == old_id) a.device_identifier = new_id; 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 // keypad buttons
std::vector<std::string> kp_lines; std::vector<std::string> kp_lines;
collect_btn_lines(keypad_buttons, kp_lines); collect_btn_lines(keypad_buttons, kp_lines);
@@ -19,6 +19,7 @@ namespace overlay::windows {
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0; int bat_threshold = 0;
unsigned short velocity_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_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
bool is_device() const { return !device_identifier.empty(); } bool is_device() const { return !device_identifier.empty(); }
@@ -34,6 +35,7 @@ namespace overlay::windows {
e.debounce_down = btn.getDebounceDown(); e.debounce_down = btn.getDebounceDown();
e.bat_threshold = btn.getBatThreshold(); e.bat_threshold = btn.getBatThreshold();
e.velocity_threshold = btn.getVelocityThreshold(); e.velocity_threshold = btn.getVelocityThreshold();
e.modifier_mask = btn.getModifierMask();
return e; return e;
} }
}; };
@@ -123,6 +125,7 @@ namespace overlay::windows {
std::string game_name; std::string game_name;
bool is_builtin = false; bool is_builtin = false;
std::vector<TemplateButtonBinding> buttons; std::vector<TemplateButtonBinding> buttons;
std::vector<TemplateButtonBinding> modifier_buttons;
std::vector<TemplateButtonBinding> keypad_buttons; std::vector<TemplateButtonBinding> keypad_buttons;
std::vector<TemplateAnalogBinding> analogs; std::vector<TemplateAnalogBinding> analogs;
std::vector<TemplateLightBinding> lights; std::vector<TemplateLightBinding> lights;
@@ -150,6 +153,7 @@ namespace overlay::windows {
bool save_user_template( bool save_user_template(
const ControllerTemplate &tmpl, const ControllerTemplate &tmpl,
const bool save_buttons, const bool save_buttons,
const bool save_modifiers,
const bool save_keypads, const bool save_keypads,
const bool save_analogs, const bool save_analogs,
const bool save_lights); const bool save_lights);
+3 -2
View File
@@ -119,8 +119,9 @@ namespace overlay::windows {
ImGui::TextDisabled("Graphics"); ImGui::TextDisabled("Graphics");
build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE); build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE);
build_button(this->overlay->window_fps.get(), "FPS", size_half, NextItem::SAME_LINE); build_button(this->overlay->window_fps.get(), "FPS", size_third, NextItem::SAME_LINE);
build_button(this->overlay->window_resize, "Resize", size_half, NextItem::NEW_LINE); build_button(this->overlay->window_obs, "OBS", size_third, NextItem::SAME_LINE);
build_button(this->overlay->window_resize, "Resize", size_third, NextItem::NEW_LINE);
ImGui::TextDisabled("I/O"); ImGui::TextDisabled("I/O");
build_button(this->overlay->window_cards, "Card Manager", size, NextItem::NEW_LINE); build_button(this->overlay->window_cards, "Card Manager", size, NextItem::NEW_LINE);
+64 -22
View File
@@ -1,6 +1,7 @@
#include <algorithm> #include <algorithm>
#include "external/fmt/include/fmt/chrono.h" #include "external/fmt/include/fmt/chrono.h"
#include "fps.h" #include "fps.h"
#include "obs.h"
namespace overlay::windows { namespace overlay::windows {
@@ -14,6 +15,7 @@ namespace overlay::windows {
this->title = "Stats"; this->title = "Stats";
this->flags = ImGuiWindowFlags_NoTitleBar this->flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_AlwaysAutoResize
| ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBringToFrontOnFocus
@@ -29,25 +31,7 @@ namespace overlay::windows {
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now()); std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
} }
void FPS::calculate_initial_window() { ImVec2 FPS::anchored_pos(const ImVec2 &size) const {
// size the window explicitly (no AlwaysAutoResize) so the corner anchoring
// below is exact; the footprint mirrors the fixed-fit table in build_content()
const float line_h = ImGui::GetTextLineHeight();
const int rows = 3;
// widest label and widest value drive the two fixed-fit columns
const float label_w = (std::max)(
ImGui::CalcTextSize("Time").x,
ImGui::CalcTextSize("Game").x);
const float value_w = ImGui::CalcTextSize("00:00:00").x;
const float win_w = label_w + value_w
+ FPS_CELL_PADDING.x * 2
+ FPS_WINDOW_PADDING.x * 2;
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * rows
+ FPS_WINDOW_PADDING.y * 2;
this->init_size = ImVec2(win_w, win_h);
// bottom-anchored windows use a larger edge margin (matching notification // bottom-anchored windows use a larger edge margin (matching notification
// toasts) since they overlap the same on-screen UI; other edges hug closer // toasts) since they overlap the same on-screen UI; other edges hug closer
const float edge_margin = overlay::apply_scaling(4); const float edge_margin = overlay::apply_scaling(4);
@@ -61,9 +45,27 @@ namespace overlay::windows {
overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft || overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft ||
overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight; overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight;
const float pos_x = right ? display.x - win_w - edge_margin : edge_margin; const float pos_x = right ? display.x - size.x - edge_margin : edge_margin;
const float pos_y = bottom ? display.y - win_h - bottom_margin : edge_margin; const float pos_y = bottom ? display.y - size.y - bottom_margin : edge_margin;
this->init_pos = ImVec2(pos_x, pos_y); return ImVec2(pos_x, pos_y);
}
void FPS::calculate_initial_window() {
// first-frame size estimate for the base 3 rows; AlwaysAutoResize handles
// the exact size (incl. any OBS rows) and build_content re-anchors each frame
const float line_h = ImGui::GetTextLineHeight();
const float label_w = (std::max)(
ImGui::CalcTextSize("Time").x,
ImGui::CalcTextSize("Game").x);
const float value_w = ImGui::CalcTextSize("00:00:00").x;
const float win_w = label_w + value_w
+ FPS_CELL_PADDING.x * 2
+ FPS_WINDOW_PADDING.x * 2;
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * 3
+ FPS_WINDOW_PADDING.y * 2;
this->init_size = ImVec2(win_w, win_h);
this->init_pos = this->anchored_pos(this->init_size);
} }
void FPS::build_content() { void FPS::build_content() {
@@ -79,6 +81,21 @@ namespace overlay::windows {
const auto uptime = now_s - this->start_time; const auto uptime = now_s - this->start_time;
// OBS status (only adds rows while streaming live or recording/paused)
OBSStatus obs_status;
bool show_stream = false;
bool show_record = false;
if (auto *obs = static_cast<OBSControl *>(this->overlay->window_obs)) {
obs_status = obs->get_status();
show_stream = obs_status.streaming;
show_record = obs_status.recording;
}
// AlwaysAutoResize sizes the window to its content, so adding/removing OBS
// rows never clips; just re-anchor it to the configured corner each frame
// using the actual (auto-sized) dimensions
ImGui::SetWindowPos(this->anchored_pos(ImGui::GetWindowSize()), ImGuiCond_Always);
// right-align a label within the current cell so the label column reads // right-align a label within the current cell so the label column reads
// flush against the value column instead of looking ragged. the label is // flush against the value column instead of looking ragged. the label is
// only slightly dimmer than normal text (not the much darker "disabled" tone) // only slightly dimmer than normal text (not the much darker "disabled" tone)
@@ -117,6 +134,31 @@ namespace overlay::windows {
fmt::format("{:%H:%M:%S}", fmt::format("{:%H:%M:%S}",
std::chrono::floor<std::chrono::seconds>(uptime)).c_str()); std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
// OBS rows - only present while live or recording
const ImVec4 col_red(0.90f, 0.30f, 0.30f, 1.0f);
const ImVec4 col_yellow(0.95f, 0.80f, 0.30f, 1.0f);
if (show_stream) {
const int64_t ms = OBSControl::live_duration_ms(
obs_status.stream_duration_ms, obs_status.stream_duration_base_tick, true);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
label("Live");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(col_red, "%s",
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
}
if (show_record) {
const int64_t ms = OBSControl::live_duration_ms(
obs_status.record_duration_ms, obs_status.record_duration_base_tick,
!obs_status.record_paused);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
label("Rec");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(obs_status.record_paused ? col_yellow : col_red, "%s",
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
}
ImGui::EndTable(); ImGui::EndTable();
} }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
+3
View File
@@ -9,6 +9,9 @@ namespace overlay::windows {
private: private:
std::chrono::system_clock::time_point start_time; std::chrono::system_clock::time_point start_time;
// anchored top-left position for a window of the given size, per FPS_LOCATION
ImVec2 anchored_pos(const ImVec2 &size) const;
public: public:
FPS(SpiceOverlay *overlay); FPS(SpiceOverlay *overlay);
@@ -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;
};
}
+290
View File
@@ -0,0 +1,290 @@
#include "obs.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include "external/imgui/imgui.h"
#include "games/io.h"
#include "overlay/overlay.h"
#include "overlay/imgui/extensions.h"
using namespace std::chrono;
// OBS WebSocket protocol/worker thread lives in obs_websocket.cpp; this file
// owns the ImGui control window and the connection lifecycle.
namespace {
// status text colors
const ImVec4 COL_GREEN(0.40f, 0.85f, 0.40f, 1.0f);
const ImVec4 COL_RED(0.90f, 0.30f, 0.30f, 1.0f);
const ImVec4 COL_YELLOW(0.95f, 0.80f, 0.30f, 1.0f);
const ImVec4 COL_GREY(0.60f, 0.60f, 0.60f, 1.0f);
// muted action-button fills (start = green, stop = red, pause = yellow); the
// hovered/active shades are derived by brightening the base
const ImVec4 COL_BTN_GREEN(0.20f, 0.45f, 0.24f, 1.0f);
const ImVec4 COL_BTN_RED(0.52f, 0.20f, 0.20f, 1.0f);
const ImVec4 COL_BTN_YELLOW(0.52f, 0.42f, 0.16f, 1.0f);
// an in-flight request lingers for at most this long before the button frees
// itself, so a dropped state event can never wedge a control permanently
const int64_t PENDING_TIMEOUT_MS = 5000;
int64_t now_tick_ms() {
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
std::string format_duration(int64_t ms) {
if (ms < 0) {
ms = 0;
}
const int64_t total_seconds = ms / 1000;
const int64_t hours = total_seconds / 3600;
const int64_t minutes = (total_seconds % 3600) / 60;
const int64_t seconds = total_seconds % 60;
char buf[16];
snprintf(buf, sizeof(buf), "%02lld:%02lld:%02lld",
static_cast<long long>(hours),
static_cast<long long>(minutes),
static_cast<long long>(seconds));
return buf;
}
}
namespace overlay::windows {
OBSControl::OBSControl(SpiceOverlay *overlay) : Window(overlay) {
this->title = "OBS Control";
this->flags |= ImGuiWindowFlags_AlwaysAutoResize;
this->init_pos = overlay::apply_scaling_to_vector(120, 120);
this->toggle_button = games::OverlayButtons::ToggleOBSControl;
this->worker_running.store(true);
this->worker_thread = std::thread(&OBSControl::worker_main, this);
}
OBSControl::~OBSControl() {
// signal stop and wake any in-progress interruptible_sleep at once; the
// lock around the store pairs with the wait predicate to avoid a lost wakeup
{
std::lock_guard<std::mutex> lock(this->worker_mutex);
this->worker_running.store(false);
}
this->worker_cv.notify_all();
if (this->worker_thread.joinable()) {
// note: if the worker is mid-connect, WebSocket::from_url performs a
// blocking getaddrinfo/connect that does not observe worker_running,
// so this join can stall for the OS connect timeout. the default
// 127.0.0.1 host fails fast (connection refused); only a misconfigured
// unreachable remote OBS_CONTROL_HOST would delay shutdown here.
this->worker_thread.join();
}
}
OBSStatus OBSControl::get_status() {
std::lock_guard<std::mutex> lock(this->status_mutex);
return this->status;
}
int64_t OBSControl::live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking) {
if (!ticking) {
return (std::max<int64_t>)(base_ms, 0);
}
const int64_t now =
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
// clamp so a stale base tick / clock hiccup can never yield a negative
// duration; callers (FPS rows, build_content) format this directly
return (std::max<int64_t>)(base_ms + (now - base_tick), 0);
}
void OBSControl::build_content() {
const OBSStatus s = this->get_status();
// label + colored value on a single line
const auto status_line = [](const char *label, const ImVec4 &col, const char *value) {
ImGui::Text("%s", label);
ImGui::SameLine();
ImGui::TextColored(col, "%s", value);
};
if (!s.connected) {
if (s.disabled) {
ImGui::TextColored(COL_GREY, "%s", "OBS Control is disabled");
return;
}
if (s.identifying) {
status_line("OBS WebSocket:", COL_YELLOW, "Connecting...");
} else {
status_line("OBS WebSocket:", COL_GREY, "Not connected");
}
const std::string url =
"ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
status_line("Address:", COL_GREY, url.c_str());
if (!s.connection_error.empty()) {
ImGui::TextColored(COL_RED, "%s", s.connection_error.c_str());
}
return;
}
status_line("OBS WebSocket:", COL_GREEN, "Connected");
// one fixed content width drives the whole panel so it never resizes as
// the scene name or button labels change; every row is sized to fit it
const float spacing = ImGui::GetStyle().ItemSpacing.x;
const float row_w = overlay::apply_scaling(240);
if (s.current_scene.empty()) {
status_line("Scene:", COL_GREY, "(unknown)");
} else {
ImGui::Text("Scene:");
ImGui::SameLine();
// truncate to the remaining row width so "Scene:" + value together
// never overflow and push the window wider
const float label_w = ImGui::CalcTextSize("Scene:").x;
ImGui::PushStyleColor(ImGuiCol_Text, COL_GREY);
ImGui::TextTruncated(s.current_scene, row_w - label_w - spacing);
ImGui::PopStyleColor();
}
ImGui::Separator();
const int64_t now = now_tick_ms();
// every button shares one fixed size; two side-by-side fill the row width,
// single buttons keep that same size rather than stretching to fill
const ImVec2 btn((row_w - spacing) * 0.5f, 0);
// has OBS reached the state a pending action was waiting for?
const auto reached = [&](OBSAction a) {
switch (a) {
case OBSAction::StreamStart: return s.streaming;
case OBSAction::StreamStop: return !s.streaming;
case OBSAction::RecordStart: return s.recording;
case OBSAction::RecordStop: return !s.recording;
case OBSAction::RecordPause: return s.record_paused;
case OBSAction::RecordResume: return !s.record_paused;
default: return true;
}
};
// drop a pending action once OBS confirms the new state, or once the
// safety deadline lapses (so a dropped event can't wedge the button)
const auto settle = [&](OBSAction &slot, int64_t deadline) {
if (slot != OBSAction::None && (reached(slot) || now >= deadline)) {
slot = OBSAction::None;
}
};
settle(this->stream_pending, this->stream_pending_deadline);
settle(this->record_pending, this->record_pending_deadline);
// a colored button that fires a request and marks the output busy on click
const auto action_button =
[&](const char *label,
const ImVec4 &color,
const char *request,
OBSAction &slot,
int64_t &deadline,
OBSAction action) {
if (ImGui::ColoredButton(label, color, btn)) {
enqueue_request(request);
slot = action;
deadline = now + PENDING_TIMEOUT_MS;
}
};
// streaming
{
const bool pending = this->stream_pending != OBSAction::None;
if (s.streaming) {
const int64_t ms = live_duration_ms(
s.stream_duration_ms, s.stream_duration_base_tick, true);
status_line("Streaming:", COL_RED, ("LIVE " + format_duration(ms)).c_str());
} else {
status_line("Streaming:", COL_GREY, pending ? "Starting..." : "Idle");
}
ImGui::BeginDisabled(pending);
if (s.streaming) {
action_button(
pending ? "Stopping...##stream" : "Stop Streaming##stream",
COL_BTN_RED,
"StopStream",
this->stream_pending,
this->stream_pending_deadline,
OBSAction::StreamStop);
} else {
action_button(
pending ? "Starting...##stream" : "Start Streaming##stream",
COL_BTN_GREEN,
"StartStream",
this->stream_pending,
this->stream_pending_deadline,
OBSAction::StreamStart);
}
ImGui::EndDisabled();
}
ImGui::Separator();
// recording
{
const bool pending = this->record_pending != OBSAction::None;
if (!s.recording) {
status_line("Recording:", COL_GREY, pending ? "Starting..." : "Idle");
ImGui::BeginDisabled(pending);
action_button(
pending ? "Starting...##record" : "Start Recording##record",
COL_BTN_GREEN,
"StartRecord",
this->record_pending,
this->record_pending_deadline,
OBSAction::RecordStart);
ImGui::EndDisabled();
return;
}
const int64_t ms = live_duration_ms(
s.record_duration_ms, s.record_duration_base_tick, !s.record_paused);
if (s.record_paused) {
status_line("Recording:", COL_YELLOW, ("PAUSED " + format_duration(ms)).c_str());
} else {
status_line("Recording:", COL_RED, ("REC " + format_duration(ms)).c_str());
}
ImGui::BeginDisabled(pending);
action_button(
this->record_pending == OBSAction::RecordStop ? "Stopping...##record" : "Stop Recording##record",
COL_BTN_RED,
"StopRecord",
this->record_pending,
this->record_pending_deadline, OBSAction::RecordStop);
ImGui::SameLine();
if (s.record_paused) {
action_button(
this->record_pending == OBSAction::RecordResume ? "Resuming...##record_toggle" : "Resume##record_toggle",
COL_BTN_GREEN,
"ResumeRecord",
this->record_pending,
this->record_pending_deadline,
OBSAction::RecordResume);
} else {
action_button(
this->record_pending == OBSAction::RecordPause ? "Pausing...##record_toggle" : "Pause##record_toggle",
COL_BTN_YELLOW,
"PauseRecord",
this->record_pending,
this->record_pending_deadline,
OBSAction::RecordPause);
}
ImGui::EndDisabled();
}
}
}

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