Compare commits

...

45 Commits

Author SHA1 Message Date
bicarus 2b930ad95f gitadora: warn about 2ch audio option not working on arena model (#702)
shrug
2026-05-25 17:00:49 -07:00
bicarus 3c60f3966b rb: more touch options (#699)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add two options:

1. option to adjust poll rate
2. option to change emulation size

Also, the default configuration is changing from:

* ~1000Hz ish to 120Hz (default)
* 3x3 point to 1x1 point

In theory this does not make a meaningful change to how the game plays.
But if people want the old behavior they can change it.

## Testing
Tested on Volzza2 and Reflesia.
2026-05-25 16:02:17 -07:00
Biduleman c59d399ab8 Automated PIN Macro after auto-inserting a card (#698)
## Description of change
Adds a way to automatically use the PIN Macro for P1, P2 or both when
Auto Card Insert is used.

### Launcher changes: 
 - Added a checkbox to enable/disable the feature
 - Added a textbox for each player to set what to look for in the logs.

### How it works:
The feature requires Auto Card Insert and PIN Macro to be enabled and
set.
It uses the Auto Card Insert value to enable the feature for P1, P2 or
both, with a "master" checkbox to completely disable the feature. It
then relies on the current PIN Macro code to send the PIN to the games.

The games logs send a message when a new scene is loaded with the name
of the scene. By hooking on the logs, we can see when a PIN is required
and send the relevant macro. Games have different scene names, and
sometimes one for each player, so the field is configurable per game per
player. These could be hard-coded in a lookup table but I didn't want to
recompile the project every time I wanted to test, and I don't have all
the games to get this info from.

The PIN Macro thread is used to hook to the logs and check if the
defined string appears, which triggers the PIN macro just like a button
press would.

Setting nothing for the log trigger disables the feature for that
player. It's useful if someone wants to have Auto Card Insert enabled
for both players but only Auto Pin on 1 player, but is mainly there so
the check won't happen if nothing is set.

## Testing
I used SDVX and DDR to have a single and a two player test with the
feature both enabled and disabled. I also tested multiple combinations
of PIN Macro settings and Auto Card Insert settings with no apparent
issues. The PIN is automatically entered as soon as the game requests
it.

## Discussion

I know that hooking to the logs might not be the best solution. I'm very
open to discussions about this, and understand that this might be a deal
breaker for this feature. I made this to use with my setup and thought
I'd try opening this PR to see if the feature would be useful for
others.

I haven't found another way to make sure we're on the correct screen to
launch the PIN Macro, and sending the macro every time the Auto Card
Insert finishes had sync issues where the end of the PIN would actually
be registered by the game.

It would also be much more convenient for the users to hardcode a lookup
table for every games' log-trigger but I don't have the games currently
so I can't check the logs to see what would be best.

---------

Co-authored-by: Maxime St-Pierre <maxime.stpierre@ticketmaster.com>
2026-05-25 16:00:06 -07:00
bicarus f69e40fa26 gitadora: (arena model) booting fullscreen with one monitor (#696)
## Link to GitHub Issue or related Pull Request, if one exists
#477 

## Description of change
By default the game needs 4 monitors.

With the new option to disable subscreens, you can now boot fullscreen
with a single 4K monitor, with subscreen overlay enabled. Side monitors
are not shown in the overlay; only the touch screen is emulated.

## Future work

- [ ] custom resolution (-forceres) doesn't work properly (hooking
ResetEx does allow the game to boot but will render incorrectly;
probably needs hex edits)
- [x] enable this automatically if the user has fewer than 4 monitors
- [ ] ability to boot with a 4k main screen + small subscreen (with no
side screens which do nothing gameplay wise), but this whole display
emulation is a nightmare to work with & test thoroughly so it'll have to
be in the future.


## Testing
Checklist:

- [x] fs on 1 monitor with option set
- [x] fs on 2 monitor with option set
- [x] windowed - default (4 windows, no overlay)
- [x] windowed - with option set (1 window, overlay)
- [x] fs with 4 monitors?
- [x] check popn hc for regression
2026-05-18 15:28:12 -07:00
bicarus e9dcc5717e launcher: option to run as user without elevation (#695)
## Link to GitHub Issue or related Pull Request, if one exists
#694 

## Description of change
Before this change, we have it set in the executable manifest to always
ask for UAC elevation.

This change removes that and instead attempts to prompt for elevation
when we launch, depending on the new developer option that is being
added here to run as current user instead of elevating.

The default behavior continues to run as admin. This is still the safest
in terms of regression risk.

A bit of history on this: while spicecfg never technically needed to
elevate, we continue to do so to avoid difference in behavior when the
game is launched. For example, if the user is using an input remapper
running without elevation, it would inject input into spicecfg running
as user but fail to do so if the game runs elevated.

WinXP builds are unaffected (there is no UAC).

Some features will straight up fail without elevation so this will
remain as a debug option. To name a few (not a complete list):

* reboot/shutdown of PC
* running at realtime process priority
* `-nvprofile` option
* ...

## Testing
2026-05-16 16:27:59 -07:00
drmext 5bec3d5db7 network: icmp emulation (#694)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Add `-icmphook` option under spicecfg Development/Network for emulating
keepalive ping replies in user mode, so games do not need to open
privileged raw ICMP sockets. This makes connecting to networks possible
on macOS under Whisky, and on Windows with ICMP firewalled or spice
running without Administrator privileges.

## Testing
Tested DDR and SDVX on Windows 10 and macOS Tahoe. Network check status
in test menu shows `CONNECTED` when -icmphook is enabled, and `NOT
CONNECTED` when it is disabled. Actual network functionality works as
expected (under previously impossible conditions).
2026-05-14 00:44:14 -07:00
bicarus-dev 6ec2b47345 fix option text 2026-05-13 17:54:25 -07:00
bicarus ebda16beae rb: update reflec beat to use new timer (#692)
## Link to GitHub Issue or related Pull Request, if one exists
Continuation of #682

## Description of change
RB uses SleepEx, so it was missed during the search while working on
#682.

## Testing
Plays about the same on a 200Hz touch monitor.
2026-05-10 19:47:33 -07:00
bicarus c29a26f183 graphics: hide subscreen window from task bar and alt+tab list when in full screen (#690)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
When booting in fullscreen, hide the subscreen from the alt+tab list and
from the task bar. Alt-tabbing to it does nothing, so this is just to
avoid confusion.

No change to windowed mode.

## Testing
Tested TDJ/SDVX/Popn. Didn't test Gitadora but it's probably fine..
2026-05-10 05:33:18 -07:00
bicarus 2014e9cf75 graphics: option to change monitor resolution (#689)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add a new option under `Monitor` group to change the monitor resolution
before booting the game, very similar to how orientation / refresh rate
options work.

This would be useful for games like Polaris Chord that boots at current
resolution but manages to have graphical issues when not in 1080p.

## Testing
2026-05-09 21:32:43 -07:00
bicarus 33f2e1d4b7 rawinput, touch: attempt to remove touchscreens injecting mouse events (#687)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #562

## Description of change

### rawinput touch fixes
In rawinput touch handler, if the mouse events (`WM_LBUTTONDOWN`,
`WM_MOUSEMOVE`, `WM_LBUTTONUP`) originated from a touch screen, drop
them on the floor instead of treating them as touch events.

This is an attempt to fix phantom touch issues we see with some screens.

Fix another issue where, if mouse is clicked inside the window, and then
dragged outside the window and button is released, touch point (id 0)
continues to be held.

### wintouchemu fixes
If we detected a touch input recently (500ms), ignore new mouse clicks.
This isn't perfect, but hopefully it helps with filtering out mouse
clicks originating from touch screens.

Also, switch over to detecting the primary mouse button correctly, not
just the left mouse button.

### notes

Future investigation: are there really touchscreens that report a mouse
HID capability & insert mouse events? We could have a way to filter them
at rawinput level but it'll be ugly.

## Testing
Tried jubeat, beatstream..
2026-05-09 17:52:43 -07:00
bicarus eb037542b4 jubeat: introduce new algorithm for touch (#686)
## Description of change
There was enough feedback that the current algorithm is not ideal for
most touch screens because most touch screens are not 24". In the
current algorithm touching the gap is no-op, so people think the game is
dropping touches.

This PR introduces a new algorithm that inspect touches on the gap and
triggers the nearest square.

## Testing
Tested landscape and portrait jubeat.
2026-05-09 04:22:29 -07:00
bicarus 039b42ad53 fix build (#684) 2026-05-08 22:46:29 -07:00
bicarus 33b4718744 overlay: fix mouse events queued up while overlay is hidden (#683)
## Link to GitHub Issue or related Pull Request, if one exists
continuation of #677

## Description of change
When overlay was hidden, when mouse buttons were clicked, they were
queued up and replayed when overlay became visible again.

This is due to improper usage of `GetAsyncKeyState`, per documentation.

## Testing
2026-05-08 03:37:46 -07:00
bicarus 71ba9b6b47 os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #681 

## Description of change
`Sleep` and `sleep_for()` can be very inaccurate and varies depending on
what the OS gives us...

### `timeBeginPeriod(1)`
On boot, we are now calling `timeBeginPeriod(1)`, which affects the
whole process but makes `Sleep` more accurate. There is some risk here
if any game was relying on doing things like `Sleep(1)` and expecting it
to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the
game engine, though not the whole time, so I'm hoping that this is not
too impactful.

### Opt out of Win11 power throttling
Ensure that timer resolution change above is respected even when the
window is occluded / minimized by opting out of throttling via
`PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`.

### Use Win10 high resolution timer instead of Sleep

On Win10 1803 and above, there is a new OS-level API for high resolution
timers; if this is available, use it
(`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE
doesn't support this currently.

If not, fall back to `Sleep`, which is significantly better than
`sleep_for()` in my experiments.

Callers of Sleep / sleep_for were replaced with this new timer. Most of
them anyway; calls to Sleep() with more than 100ms+ was left alone.

### Add an option as a chicken bit

To opt out I'm adding a new option called `Use Legacy Timers` which will
revert to behavior before this PR. The code paths that switched from
`sleep_for` to `Sleep` will remain in place though, not affected by the
option.

## Expected changes
In some I/O emulation modules, poll threads may run more frequently,
resulting in lower latency.

It also means that spice overall may use more CPU resources and power.
If you don't like this, you can always enable the option to opt out;
e.g., if you're on old arcade cab PC.

## Testing

DDR p4io - ok
drs touch hook - ok
IIDX camera hook - ok
CCJ trackball - ok
2026-05-08 02:25:20 -07:00
bicarus 1957917270 sdk: update cpp sample for DDR, fix DDR MDXF polling when api/sdk is providing input (#679)
## Link to GitHub Issue or related Pull Request, if one exists
#676 
#452 

## Description of change

When spice SDK or API sets or clears button and analog override, we
don't tell MDXF about it, so it may fall behind. This is a special case
for DDR MDXF I/O emulation. For Naive/Xinput I think we are getting
lucky because they usually also generate WM_INPUT events.

Update the C++ SDK sample to provide more interesting example of
emulating DDR arrows with the keyboard.

## Testing
2026-05-07 18:10:14 -07:00
bicarus b9ccddf47d launcher: prevent early hook from launching in standalone configurator (#678)
## Description of change
Seems to be an oversight when it was initially implemented; hooks should
not load when launching `spicecfg` (or `spice -cfg` or `spice64 -cfg`).
2026-05-07 00:26:18 -07:00
bicarus cd0ba51b5a sdk: introduce Spice SDK (#676)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds a flat-C, headers-only library for writing DLL plugins with spice.

This is meant to be an alternative to writing applications over spice
API which doesn't suit some scenarios like writing custom
high-performance I/O modules.

All you have to do is:

1. Create a 32-bit or 64-bit DLL with `spice_sdk_entry_point` as a
DLLExport
1. Include `spicesdk.h`, and optionally `spicesdk_io.h` (see
`extras\sdk\include`)
1. `spice_sdk_entry_point` will be called when hooks get initialized.
Call `init` with valid parameters.
1. When `init` returns, `SPICE_SDK_V0` will be filled out with a series
of function pointers; your DLL can now start calling into it.
1. Compile your DLL and add it as a DLL hook (`-k`) in spice when
launching the game.

See `v0_cpp.cpp` for an example.

V0 implements the following functions:

```
    log;

    get_game_info;
    get_avs_info;

    get_button;
    set_button;

    get_analog;
    set_analog;

    get_light;
    set_light;

    set_touch;
    clear_touch;

    insert_card;
    set_keypad;
```

V0 is focused on providing common utility functions needed in a lot of
DLL hooks (get AVS info, log messages to file), and implementing I/O.

In the future, we could have more interesting things like:

* scanning and modifying memory 
* installing DLL hooks
* having direct hooks into I/O emulation modules
* getting full tape LEDs
* having a callback into DX9 `Present` call
* drawing an ImGui window

More detailed documentation is coming soon in form of a wiki page.

## Testing
See included sample DLL.
2026-05-06 23:29:52 -07:00
bicarus 1f23d88c7a overlay: prevent overlay from queueing up keyboard input while overlay is not visible (#677)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #675

Yet another regression from #604 

## Description of change
When overlay was hidden (not visible) it was still accepting keyboard
input, so when it became visible again the queued up keystrokes got
processed all at once.

Also, fix a bug with monitor settings update kicking in when in windowed
mode - this really should have been only for full screen.

## Testing
Tested DDR windowed / full screen, TDJ with subscreen overlay windowed /
fullscreen.
2026-05-06 01:00:52 -07:00
bicarus-dev 46e43ab09c fix typo 2026-05-04 23:57:35 -07:00
bicarus 96950b6b4e rawinput: reimplement analog relative mode and delay (#674)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Implement analog relative mode and delay option using a new
millisecond-based algorithm.

## Testing
WIP
2026-05-04 22:50:11 -07:00
bicarus 5f94ef4478 overlay: fix scroll bar jumping around when regaining focus (#673)
## Link to GitHub Issue or related Pull Request, if one exists
Regression caused by #604, probably

## Description of change
When spicecfg or the game loses focus and regains it, scroll bar jumps
around. This was because we weren't keeping proper track of mouse wheel
position while not in focus, so when we gain it back, it's as if the
scroll bar was moved a huge distance.

## Testing
Tested spicecfg and in overlay.
2026-05-03 03:49:52 -07:00
bicarus c4193eddc5 iidx: millisecond-based delay option (#672)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Use millisecond-based calculation to add artificial delay to the TT
input.

## Testing
Tested 64 bit TDJ and LDJ.
2026-05-03 01:23:45 -07:00
bicarus c8962a0e77 overlay: setting button as always-on (#671)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Add a checkbox to the buttons edit dialog that lets you set a device as
"always on", accomplished by setting the device to `Naive`, vkey to
`0xff` (invalid), and `Invert`.

No backend rawinput changes.

## Testing
2026-05-02 22:42:36 -07:00
din b6a749e5db add snek to lights match map (#670)
## Description of change
add snek board to lights mapping

## Testing
with a snek board


<img width="490" height="477"
alt="DeepinScreenshot_select-area_20260502164249"
src="https://github.com/user-attachments/assets/a86affff-74c6-4d5d-b657-fab12c40aaa4"
/>
2026-05-02 15:49:19 -07:00
bicarus-dev 7dbbe9809e update iidx tt delay option text 2026-05-02 02:50:29 -07:00
bicarus 79a3e32d5d iidx: delay logic for iidx tt (#669)
## Link to GitHub Issue or related Pull Request, if one exists
Re-implements fix for #181 as it was removed by #668

## Description of change
Add two options to add artificial delay to turntable input. These
operate directly on the input logic (`get_tt`) which is called by I/O
emulation code.

Functionally, it's **identical** to the `Delay (experimental)` option we
had in the analog tab, just moved to Options tab.

## Testing
64 bit tdj: ok
64 bit ldj bi2a: ok
32 bit ldj ezusb: ok

Worth noting that TDJ bi2x_hook polls at 120Hz, bi2a at ~500Hz, and
ezusb at ~170Hz
2026-05-02 02:17:51 -07:00
bicarus c5a4e954f9 rawinput: remove experimental analog features (delay and relative axis mode) (#668)
## Link to GitHub Issue or related Pull Request, if one exists
Related to #181 

## Description of change
This PR removes two features marked as "experimental" for analog axis -
`Delay` and `Relative Axis`.

They have been in spice as experimental option for almost 2 years.. but
they have a fundamental problem of being tied to how often the analog is
polled. Not just how often the game's I/O emulation code polls it, but
literally any source (spice API, overlay, etc).

For `Delay`, the only real usage I'm aware of is for #181 which is
delaying IIDX turntable input. For this, I will follow up with a
separate PR that adds back the delay option specifically for IIDX.

For `Relative Axis` - I'm not quite sure if any one actually uses this.
This could be implemented per-game if some game demands it. Polaris
Chord already has one, for example.

## Testing
2026-05-02 00:51:21 -07:00
bicarus 7b862768b0 overlay: show name of analog control in popup title (#667) 2026-05-02 00:01:33 -07:00
bicarus af8d8dae9f rawinput: distinguish between circular and linear analog input (#665)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Currently, all analogs are treated as circular values that wrap around.
This works for knobs and turntables, but can get weird for linear values
(like tilt sensors and sliders) especially once analog options are
enabled, such as sensitivity.

This PR groups analogs into 3 buckets: circular, linear (centered), and
linear (positive). Linear types get different algorithms applied to it
when options are set (e.g., sensitivity caps out at 0, 1 instead of
wrapping around).

In linear mode:
* Sensitivity value applies an exponential response curve (power curve)
to the original value.
* Multiplier/divisor serves as a linear cut-off.
* Relative mode works as you expect, but of course, values don't wrap
around.
* Invert and deadzone work as you expect (since they applied to the
controller input anyway)
* Smoothing is removed from the UI and has no effect.
* Delay is unaffected.

Also, fix a small bug with -/+ button not working for analog `Delay`
option.

## Testing
WIP
2026-05-01 19:02:32 -07:00
llm96 d9d5823fdb rawinput: add missing nullptr check (#666)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change

Fixes a crash when opening the properties modal on a Naive button bind.


https://github.com/spice2x/spice2x.github.io/blob/82dda3380039bbaa995ee99e2d3effebbcb58082/src/spice2x/overlay/windows/config.cpp#L2081

Binds:
<img width="800" height="600" alt="image"
src="https://github.com/user-attachments/assets/18d1fcfb-085f-4a2b-8fce-36809a4bbb5a"
/>

Stack trace from Debug build:
```
overlay::windows::Config::edit_button_popup(const std::string &, const std::string &, Button *, float, int) config.cpp:2081
overlay::windows::Config::build_button(const std::string &, Button &, Button *, int, int, int) config.cpp:1050
overlay::windows::Config::build_buttons(const std::string &, std::vector<…> *, int, int) config.cpp:718
overlay::windows::Config::build_content() config.cpp:283
overlay::Window::build() window.cpp:96
overlay::SpiceOverlay::new_frame() overlay.cpp:486
graphics_d3d9_on_present(HWND__ *, IDirect3DDevice9 *, IDirect3DDevice9 *) d3d9_backend.cpp:1447
__ZN23WrappedIDirect3DDevice97PresentEPK7tagRECTS2_P6HWND__PK8_RGNDATA@20 d3d9_device.cpp:396
main_implementation(int, char **) launcher.cpp:2453
main launcher.cpp:2671
```

Relevant snippet from error log:
```
[2026/05/01 18:50:17] M:config: tab selection changed from 0 to 1
[2026/05/01 18:50:48] W:signal: exception raised: EXCEPTION_ACCESS_VIOLATION
[2026/05/01 18:50:48] W:troubleshooter: 

/-------------------------- spice2x auto-troubleshooter -----------------------\

  spice2x version: 2026-04-23
  game version: LDJ:J:A:A:2013090900

  the game has crashed
    * share this entire log file with someone for troubleshooting (log.txt)
    * spice will also attempt to create a minidump (minidump.dmp)
        minidump should only be shared with people you trust as it may contain
        sensitive data (PCBID, card ID, etc)

  log level is set to `disable` (either in avs-config.xml or using -loglevel)
      this log file may have omitted important error messages from the game
      if you are troubleshooting crashes or failures, it is recommended that you
      set AVS Log Level (-loglevel) option to `all`

  unsure what to do next?
    * update to the latest version:
        https://github.com/spice2x/spice2x.github.io/releases
    * check the FAQ:
        https://github.com/spice2x/spice2x.github.io/wiki/Known-issues

\------------------------- spice2x auto-troubleshooter ------------------------/

[2026/05/01 18:50:48] I:signal: printing callstack
[2026/05/01 18:50:48] I:stackwalker: 006A7001 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0068DF8C (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069A7C1 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069B857 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0066461F (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 00663B7A (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0045B548 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 004FE1CE (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 1008AD13 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 100934A7 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 10088D24 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 0044B8A6 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 77290CB7 (ntdll): (unknown): RtlGetSystemTimeAndBias
[2026/05/01 18:50:53] F:signal: end
[2026/05/01 18:50:53] I:launcher: stopping subsystems
[2026/05/01 18:50:53] I:logger: stop
[2026/05/01 18:50:53] I:audio: stopping
[2026/05/01 18:50:54] I:rawinput: disposing devices
[2026/05/01 18:50:54] I:xinput: destroyed
```

## Testing
Replicated on upstream build from `spice2x-26-04-23-full.zip`
2026-05-01 14:42:45 -07:00
bicarus 82dda33800 graphics: restore monitor settings when fullscreen is lost then restored (#664)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #663 

## Description of change
Apply monitor rotation and refresh rate again when fullscreen focus is
regained.

## Testing
Tested with sdvx7
2026-04-28 16:04:52 -07:00
bicarus 3876e38636 xinput: small fixes (#661)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
Fix UI labels being wrong for analog stick up/down (they were reversed).

Adjust deadzone so that when sticks are bound as digital buttons, they
are not as sensitive.

Small refactoring of code.

## Testing
Tested via spicecfg using my xbox one controller.
2026-04-27 22:19:23 -07:00
bicarus 6bc1357d6a ddr: add support for analog axis-only pads and handle left+right / up+down input (#659)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Some crappy DDR soft mats (either USB-based or console pads with crappy
adapters that lack a dedicated "DDR pad mode") use X and Y analog axis
for arrows, which means they cannot cleanly report Up+Down or Left+Right
input.

According to some ancient Stepmania code, a lot of these report a middle
value (value between center and right, for example, for left+right).
Borrow this hack from Stepmania and implement it in spice as analog
input for DDR.

Also, for rawinput/xinput automatic bind scenario, update the logic so
that face buttons are always detected before analog axis / dpads for
consistency. This would help with binding any DDR pads that
simultaneously input face buttons / dpad / analog axis, so that we can
prefer face buttons when detected.

## Testing
I don't have any soft mats that have this, but it was tested with an
xbox controller.
2026-04-27 20:17:54 -07:00
Will 37218e7fe0 Try and preserve the wideness of std::filesystem::path more (#660)
As noted in #567, a filesystem path that contains non-ascii will break a
lot if using a clang toolchain.

Luckily, fmtlib has a lossy utf8 convert when you use it to print a path
(after including `fmt/std.h`). The vast majority of this diff is just
removing `.string()` from paths inside loggings calls.

There are some callsites I _didn't_ touch, mainly the options, because
it would be an ABI break to change those to be wide strings and I cbf
looking into settings upgrades. There are also some spots (avs mountpath
remapping, for example) where the path is guaranteed to be ascii, so I
didn't modify them.

ImGui doesn't appear to easily support wide strings (I mean, surely it
does, but I'm not gonna look too far into it) so I mostly just left
those alone too, with a few spots modified to re-use fmtlib's lossy
utf8.

Some of the changes are basically never gonna be hit IRL, like who would
put a file with a non-ascii _extension_ along with their modules? But
the diff is (I hope) pretty easy to validate as OK.

Testing has been somewhat minimal, I fired up the GCC build of spice2x
in a dodgy folder name, got mojibake (running via wine in linux so take
that as you will), ran the unmodified clang spice and crashed the same
way the reporter did. After modification, I get the exact same mojibake
so I assume if the terminal enjoys utf8 it'll display OK.

Claude (only used for review) thinks the commit is fine but is annoyed
that I use `fmt::detail` in the appdata censoring, which is part of the
private API; personally I don't care because it's pretty stable.
2026-04-27 20:07:15 -07:00
bicarus 678e11eade rawinput: fix up devices that report invalid max values (#658)
## Link to GitHub Issue or related Pull Request, if one exists
regression caused by #653

## Description of change
Some controllers - like the Xbox One controller I have - reports
0xffffffff as the max range value for analog, despite reporting 16-bit
width. This causes us to consider 0xffffffff as -1 so the range became
[0, -1] which is invalid.

Our automatic calibration does eventually fix this (-1 will fix itself
to become the highest actual value reported, which would be 0xffff).

This seems to be a common issue with XInput-based controllers in
general: see `Correct_Axis_Max` in
https://github.com/argonlefou/DemulShooter/blob/master/DsCore/RawInput/RawInputController.cs

#653 removed the fix that was masking this issue. Put in a new fix that
fixes these up.

## Testing
Tested with Xbox One controller.
2026-04-27 01:41:44 -07:00
bicarus f72313fe45 sdvx: work around bad logger configuration (#657)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
SDVX4 data from certain private forum has bad logger configuration and
crashes on boot. Add an explicit workaround to fix up that broken
`avs-config.xml` file, scoped to `soundvoltex.dll` + 32-bit.

## Testing
Tested HH.
2026-04-23 01:33:49 -07:00
bicarus-dev 06374ef78a cosmetic fix to build script 2026-04-22 23:14:03 -07:00
bicarus-dev bdde1ec6b8 update ifs_hook missing warning message 2026-04-22 22:56:55 -07:00
bicarus-dev d2176fe4f0 clear out button properties when switching devices in edit dialog 2026-04-22 22:48:29 -07:00
bicarus fb9eef904a popn: block keypad overlay window in new cab i/o (#656)
Redirect users to the subscreen overlay when keypad window is open.
2026-04-22 22:21:18 -07:00
bicarus 48903998a5 ea3: fix boot failure when -e is specified to override ea3-config path (#655)
## Link to GitHub Issue or related Pull Request, if one exists
Regression introduced by #595 

## Description of change
`avs::core::file_exists` fails if you use `\` as path separator... what
I meant to use was `fileutils::file_exists`.

This would have caused failures - mostly for gitadora players - if they
used `prop\ea3-config2.xml` for example.

## Testing
2026-04-22 01:55:13 -07:00
bicarus f2dca0265f rawinput: custom threshold for BAT (button-analog type) (#654)
## Link to GitHub Issue or related Pull Request, if one exists
#653 

## Description of change
By default, BAT Positive / Negative binds trigger when the stick is
tilted 10% off the center.

This needs a bit more customization since:

1. User may want to customize the center point (e.g., guitar controller
neutral position)
2. User may want to adjust the sensitivity (e.g., guitar controller tilt
angle to trigger wailing).

## Testing
WIP
2026-04-22 01:14:15 -07:00
bicarus 1147ed9858 rawinput: fix signed range values (#653)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Official GuitarFreaks controller has the motion sensor values with
`valuemin=-127` to `valuemax=127` and we don't deal with negative values
correctly.

## Testing
tbd
2026-04-21 22:56:27 -07:00
bicarus 19d93f4ffd audio: deal with missing DirectSoundI3DL2ReverbDMO better (#652)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
On some Win11 systems,
`CoCreateInstance(CLSID_DirectSoundI3DL2ReverbDMO)` started to fail with
`ERROR_MOD_NOT_FOUND` instead of `REGDB_E_CLASSNOTREG` for some reason.

## Testing
User tested.
2026-04-21 18:42:21 -07:00
121 changed files with 6871 additions and 996 deletions
+38 -2
View File
@@ -523,6 +523,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/lang.cpp hooks/lang.cpp
hooks/libraryhook.cpp hooks/libraryhook.cpp
hooks/networkhook.cpp hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/powrprof.cpp hooks/powrprof.cpp
#hooks/rom.cpp #hooks/rom.cpp
hooks/setupapihook.cpp hooks/setupapihook.cpp
@@ -607,6 +609,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/structuredmessage.cpp reader/structuredmessage.cpp
reader/crypt.cpp reader/crypt.cpp
# sdk
sdk/sdk.cpp
# stubs # stubs
stubs/stubs.cpp stubs/stubs.cpp
@@ -633,6 +638,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/time.cpp util/time.cpp
util/cpuutils.cpp util/cpuutils.cpp
util/netutils.cpp util/netutils.cpp
util/precise_timer.cpp
util/sysutils.cpp util/sysutils.cpp
util/lz77.cpp util/lz77.cpp
util/tapeled.cpp util/tapeled.cpp
@@ -860,6 +866,36 @@ if(NOT MSVC)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif() endif()
# sdk_sample_v0_flat_c.dll (32 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_32 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# output directories # output directories
#################### ####################
@@ -869,14 +905,14 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
# output 32bit # output 32bit
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32
PROPERTIES PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
# output 64bit # output 64bit
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64
PROPERTIES PROPERTIES
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"
+10
View File
@@ -283,6 +283,16 @@ Python is the only one that is fully spec compliant.
Other libraries may be missing features and contain bugs; please feel free to Other libraries may be missing features and contain bugs; please feel free to
contribute code to fill the gaps if you work on a project using these libraries. contribute code to fill the gaps if you work on a project using these libraries.
## Spice SDK (for DLL hooks / plugins)
As an alternative to Spice API, Spice SDK is available.
This is a flat C, header-only library that can be included in your DLL. Once
initialized, you can call directly into helper routines provided by Spicetools
executable, making low-latency and high-throughput interactions possible.
Check the documentation on the wiki for more details.
## License ## License
Unless otherwise noted, all files are licensed under the GPLv3. Unless otherwise noted, all files are licensed under the GPLv3.
See the LICENSE file for the full license text. See the LICENSE file for the full license text.
+23 -8
View File
@@ -1,13 +1,16 @@
#include "mdxf.h" #include "mdxf.h"
#include "mdxf_poll.h" #include "mdxf_poll.h"
#include <mutex>
#include "avs/game.h" #include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h" #include "games/ddr/io.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include <mutex>
#define MDFX_DEBUG_VERBOSE 0 #define MDFX_DEBUG_VERBOSE 0
@@ -108,9 +111,10 @@ static void mdxf_thread_start() {
MDXF_THREAD = std::thread([] { MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
timeutils::PreciseSleepTimer timer;
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) { while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
mdxf_poll(false); mdxf_poll(false);
std::this_thread::sleep_for(THREAD_PERIOD); timer.sleep(THREAD_PERIOD);
} }
}); });
} }
@@ -357,14 +361,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// decide on button map // decide on button map
const size_t *button_map = nullptr; const size_t *button_map = nullptr;
int player = 0;
switch (node) { switch (node) {
case 17: case 17:
case 25: case 25:
button_map = &buttons_p1[0]; button_map = &buttons_p1[0];
player = 1;
break; break;
case 18: case 18:
case 26: case 26:
button_map = &buttons_p2[0]; button_map = &buttons_p2[0];
player = 2;
break; break;
} }
@@ -374,19 +381,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
if (source == EXTERNAL_POLL) { if (source == EXTERNAL_POLL) {
// get buttons // get buttons
auto &buttons = games::ddr::get_buttons(); auto &buttons = games::ddr::get_buttons();
// get analogs
bool analog_left = false;
bool analog_right = false;
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
bool analog_up = false;
bool analog_down = false;
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
uint8_t up_down = 0; uint8_t up_down = 0;
uint8_t left_right = 0; uint8_t left_right = 0;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
up_down |= 0xF0; up_down |= 0xF0;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
up_down |= 0x0F; up_down |= 0x0F;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
left_right |= 0xF0; left_right |= 0xF0;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
left_right |= 0x0F; left_right |= 0x0F;
} }
current_state = (uint16_t(up_down) << 8) | left_right; current_state = (uint16_t(up_down) << 8) | left_right;
@@ -500,4 +515,4 @@ acio::MDXFModule::~MDXFModule() {
if (IS_THREAD_NEEDED) { if (IS_THREAD_NEEDED) {
mdxf_thread_stop(); mdxf_thread_stop();
} }
} }
+1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <string> #include <string>
#include "util/circular_buffer.h" #include "util/circular_buffer.h"
+3 -1
View File
@@ -5,6 +5,7 @@
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
using namespace acioemu; using namespace acioemu;
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling // keypad thread for faster polling
if (keypad_thread) { if (keypad_thread) {
this->keypad_thread = new std::thread([this]() { this->keypad_thread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
while (this->cards) { while (this->cards) {
for (int unit = 0; unit < this->node_count; unit++) { for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false); this->update_keypad(unit, false);
} }
Sleep(7); timer.sleep(7);
} }
}); });
} }
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "games/io.h" #include "games/io.h"
#include "util/utils.h" #include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -104,6 +105,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) { for (auto &analog : *this->analogs) {
analog.override_enabled = false; analog.override_enabled = false;
} }
mdxf_poll(true);
} }
return; return;
} }
@@ -148,6 +150,7 @@ namespace api::modules {
if (analog.getName() == name) { if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f); analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true; analog.override_enabled = true;
mdxf_poll(true);
return true; return true;
} }
} }
@@ -167,6 +170,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) { for (auto &analog : *this->analogs) {
if (analog.getName() == name) { if (analog.getName() == name) {
analog.override_enabled = false; analog.override_enabled = false;
mdxf_poll(true);
return true; return true;
} }
} }
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "games/io.h" #include "games/io.h"
#include "util/utils.h" #include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -107,6 +108,7 @@ namespace api::modules {
for (auto &button : *this->buttons) { for (auto &button : *this->buttons) {
button.override_enabled = false; button.override_enabled = false;
} }
mdxf_poll(true);
} }
return; return;
} }
@@ -153,6 +155,7 @@ namespace api::modules {
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED; GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f); button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true; button.override_enabled = true;
mdxf_poll(true);
return true; return true;
} }
} }
@@ -172,6 +175,7 @@ namespace api::modules {
for (auto &button : *this->buttons) { for (auto &button : *this->buttons) {
if (button.getName() == name) { if (button.getName() == name) {
button.override_enabled = false; button.override_enabled = false;
mdxf_poll(true);
return true; return true;
} }
} }
+4 -2
View File
@@ -7,6 +7,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -58,6 +59,7 @@ namespace api::modules {
// get params // get params
auto keypad = req.params[0].GetUint(); auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString()); auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars // process all chars
for (auto c : input) { for (auto c : input) {
@@ -91,11 +93,11 @@ namespace api::modules {
// set // set
eamuse_set_keypad_overrides(keypad, state); eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time); timer.sleep(sleep_time);
// unset // unset
eamuse_set_keypad_overrides(keypad, 0); eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time); timer.sleep(sleep_time);
} }
} }
+3 -3
View File
@@ -58,7 +58,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
@@ -118,7 +118,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
@@ -196,7 +196,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
+3 -1
View File
@@ -5,6 +5,7 @@
#include <utility> #include <utility>
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
@@ -16,6 +17,7 @@ namespace api {
controller->init_state(this->state); controller->init_state(this->state);
this->thread = new std::thread([this] () { this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud); log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer // read buffer
uint8_t read_buffer[16*1024]; uint8_t read_buffer[16*1024];
@@ -160,7 +162,7 @@ namespace api {
// slow down on reconnect // slow down on reconnect
if (this->running) { if (this->running) {
Sleep(retry_time); timer.sleep(retry_time);
} }
} }
}); });
+16 -7
View File
@@ -1168,7 +1168,7 @@ namespace avs {
// Ongaku Paradise // Ongaku Paradise
{"arkjc9.dll", 0xA00000}, {"arkjc9.dll", 0xA00000},
// KAMUNITY // KAMUNITY
{"kamunity.dll", 33554432}, {"kamunity.dll", 33554432},
}; };
@@ -1516,7 +1516,7 @@ namespace avs {
" * It's also possible that you have incomplete game data\n" " * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n" " * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n" "\n"
, DLL_NAME, MODULE_PATH.string()) }; , DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str); log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME); log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME);
} }
@@ -1757,9 +1757,9 @@ namespace avs {
{ {
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"AVS filesystem initialization failure was previously detected during boot!", "AVS filesystem initialization failure was previously detected during boot!",
fmt::format(" ERROR: directory could not be created: {}", src_path.string().c_str()), fmt::format(" ERROR: directory could not be created: {}", src_path),
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH.c_str()), fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH),
" fix the XML file and try again" " fix the XML file and try again"
}); });
} }
@@ -1780,14 +1780,14 @@ namespace avs {
auto created = std::filesystem::create_directories(real_path, err); auto created = std::filesystem::create_directories(real_path, err);
if (created) { if (created) {
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path.string()); log_info("avs-core", "created '{}' at '{}'", avs_path, real_path);
} }
if (err) { if (err) {
avs_dir_err(real_path); avs_dir_err(real_path);
log_warning("avs-core", "failed to create '{}' folder at '{}': {}", log_warning("avs-core", "failed to create '{}' folder at '{}': {}",
avs_path, avs_path,
real_path.string(), real_path,
err.message()); err.message());
} }
} }
@@ -1921,6 +1921,15 @@ namespace avs {
// create nvram and raw directories if possible for mounttable configurations // create nvram and raw directories if possible for mounttable configurations
create_avs_config_fs_table(config, config_node); create_avs_config_fs_table(config, config_node);
#if !SPICE64
// sdvx4 bad log config fix
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
property_search_safe(config, config_node, "/log/enable_console")) {
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
property_search_remove_safe(config, config_node, "/log/enable_console");
}
#endif
// set log level // set log level
if (!LOG_LEVEL_CUSTOM.empty()) { if (!LOG_LEVEL_CUSTOM.empty()) {
property_search_remove_safe(config, config_node, "/log/level"); property_search_remove_safe(config, config_node, "/log/level");
+6 -4
View File
@@ -151,8 +151,8 @@ namespace avs {
" * It's also possible that you have incomplete game data\n" " * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n" " * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n" "\n"
, DLL_NAME, MODULE_PATH.string()) }; , DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str); log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME); log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
} }
@@ -217,8 +217,10 @@ namespace avs {
ea3_config_name = "prop/eamuse-config.xml"; ea3_config_name = "prop/eamuse-config.xml";
} }
if (avs::core::file_exists(ea3_config_name)) { if (fileutils::file_exists(ea3_config_name)) {
log_info("avs-ea3", "booting (using {})", ea3_config_name); log_info("avs-ea3", "found {} on disk", ea3_config_name);
} else if (CFG_PATH.size()) {
log_fatal("avs-ea3", "user-specified ea3 config file is missing: {}", ea3_config_name);
} else { } else {
log_warning("avs-ea3", "looked for the following files in order:"); log_warning("avs-ea3", "looked for the following files in order:");
log_warning("avs-ea3", " * prop/ea3-config.xml"); log_warning("avs-ea3", " * prop/ea3-config.xml");
+4 -5
View File
@@ -73,11 +73,10 @@ namespace avs {
// load game instance // load game instance
const auto dll_path = MODULE_PATH / DLL_NAME; const auto dll_path = MODULE_PATH / DLL_NAME;
const auto dll_path_s = dll_path.string(); log_info("avs-game", "DLL path: {}", dll_path);
log_info("avs-game", "DLL path: {}", dll_path_s.c_str());
// MAX_PATH is 260 // MAX_PATH is 260
if (130 <= dll_path_s.length()) { if (const auto dll_path_len = dll_path.wstring().length(); 130 <= dll_path_len) {
log_warning( log_warning(
"avs-game", "avs-game",
"PATH TOO LONG WARNING\n\n" "PATH TOO LONG WARNING\n\n"
@@ -93,7 +92,7 @@ namespace avs {
"long, often resulting in random crashes. Move the game contents to\n" "long, often resulting in random crashes. Move the game contents to\n"
"a directory with shorter path.\n" "a directory with shorter path.\n"
"-------------------------------------------------------------------\n\n", "-------------------------------------------------------------------\n\n",
dll_path_s, dll_path_s.length()); dll_path, dll_path_len);
} }
// ddr gamemdx.dll user error // ddr gamemdx.dll user error
@@ -112,7 +111,7 @@ namespace avs {
// file not found on disk // file not found on disk
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path.string().c_str()); log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path);
log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank"); log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank");
} }
+37 -37
View File
@@ -39,8 +39,8 @@
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/> <button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons> </buttons>
<analogs> <analogs>
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs> </analogs>
<lights> <lights>
<light name="P1 1" devid="Player 1" index="0"/> <light name="P1 1" devid="Player 1" index="0"/>
@@ -117,8 +117,8 @@
<keypad_buttons/> <keypad_buttons/>
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. --> <!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
<analogs> <analogs>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs> </analogs>
<lights> <lights>
<light name="P1 1" devid="arcin (1p)" index="0"/> <light name="P1 1" devid="arcin (1p)" index="0"/>
@@ -144,34 +144,34 @@
<buttons/> <buttons/>
<keypad_buttons/> <keypad_buttons/>
<analogs> <analogs>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs> </analogs>
<lights> <lights>
<light name="Key 1 R" devid="Lights 01-14" index="0"/> <light name="Key 1 R" devid="Lights 01-14" index="0"/>
@@ -277,9 +277,9 @@
</buttons> </buttons>
<keypad_buttons/> <keypad_buttons/>
<analogs> <analogs>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs> </analogs>
<lights/> <lights/>
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens --> <!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
@@ -296,8 +296,8 @@
</buttons> </buttons>
<keypad_buttons/> <keypad_buttons/>
<analogs> <analogs>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/> <analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs> </analogs>
<lights> <lights>
<light name="BT-A" devid="Lights (MI_02)" index="24"/> <light name="BT-A" devid="Lights (MI_02)" index="24"/>
+1 -1
View File
@@ -23,7 +23,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges> </requestedPrivileges>
</security> </security>
</trustInfo> </trustInfo>
+12 -14
View File
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip" DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip" DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip"
DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.' DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.'
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm" TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux" TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64"
TARGETS_XP32="spicetools_cfg spicetools_spice" TARGETS_XP32="spicetools_cfg spicetools_spice"
TARGETS_XP64="spicetools_spice64" TARGETS_XP64="spicetools_spice64"
@@ -163,6 +163,7 @@ time (
if ((BUILD_XP > 0)) if ((BUILD_XP > 0))
then then
# 32 bit Windows XP # 32 bit Windows XP
echo ""
echo "Building 32bit targets (WinXP toolchain)..." echo "Building 32bit targets (WinXP toolchain)..."
echo "=========================" echo "========================="
if ((CLEAN_BUILD > 0)) if ((CLEAN_BUILD > 0))
@@ -187,6 +188,7 @@ time (
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64} CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64}
popd > /dev/null popd > /dev/null
else else
echo ""
echo "Skipping WinXP builds, toolchain not specified" echo "Skipping WinXP builds, toolchain not specified"
fi fi
@@ -267,6 +269,8 @@ rm -rf ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS} mkdir -p ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware 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/64
if ((BUILD_XP > 0)) if ((BUILD_XP > 0))
then then
mkdir -p ${OUTDIR_EXTRAS}/winxp mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -279,16 +283,8 @@ then
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
else else
# release files # release files
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
@@ -296,16 +292,15 @@ else
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
if ((BUILD_XP > 0)) if ((BUILD_XP > 0))
then then
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
@@ -324,6 +319,9 @@ then
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?" echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
fi fi
# sdk headers
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
# copy resources # copy resources
rm -rf ${OUTDIR_EXTRAS}/api rm -rf ${OUTDIR_EXTRAS}/api
mkdir ${OUTDIR_EXTRAS}/api mkdir ${OUTDIR_EXTRAS}/api
+2 -2
View File
@@ -1,7 +1,7 @@
docker build --pull external/docker -t spicetools/deps docker build --pull external/docker -t spicetools/deps
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
docker run --rm -it -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %* docker run --rm -i -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
@REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below @REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb @REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb @REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb @REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
+102 -15
View File
@@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) {
} }
float Analog::normalizeAnalogValue(float value) { float Analog::normalizeAnalogValue(float value) {
// effectively the same as fmodf(value, 1.f) if (getType() == GameAPI::Analogs::AnalogType::Circular) {
// for small values, this is MUCH faster than fmodf. // effectively the same as fmodf(value, 1.f)
float new_value = value; // for small values, this is MUCH faster than fmodf.
while (new_value > 1.f) { float new_value = value;
new_value -= 1.f; while (new_value > 1.f) {
new_value -= 1.f;
}
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
} else {
// clamp to [0, 1] range
return std::clamp(value, 0.f, 1.f);
} }
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
} }
float Analog::applyDeadzone(float raw_value) { float Analog::applyDeadzone(float raw_value) {
float value = raw_value; float value = raw_value;
const auto deadzone = this->getDeadzone(); auto deadzone = this->getDeadzone();
if (deadzone > 0) {
// in the past, positive deadzone applied in the center, negative deadzone applied to 0
// after each analog value received a type (circular/linear) this has been simpliifed to
// positive values only since we can figure out where the rest value is
// for back compat, treat negative value as positive
if (deadzone < 0.f) {
deadzone = -deadzone;
}
// relative mode assumes that user is using a stick, so center is neutral regardless of analog type
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) {
// calculate values // calculate values
const auto delta = value - 0.5f; const auto delta = value - 0.5f;
@@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
} }
} }
} else if (deadzone < 0) { } else {
// invert for mirror // invert for mirror
if (this->getDeadzoneMirror()) { if (this->getDeadzoneMirror()) {
@@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
} }
// deadzone from minimum value // deadzone from minimum value
if (deadzone > -1 && value > -deadzone) { if (deadzone < 1.f && deadzone < value) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone)); value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
} else { } else {
value = 0.f; value = 0.f;
} }
@@ -275,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
} }
} }
return value; return value;
} }
float Analog::getRelativeModeValue(float raw_value) {
const auto now = get_performance_seconds();
auto delta_time = now - this->rel_mode_last_read_time_s;
if (this->rel_mode_last_read_time_s != 0.f) {
// some heuristics to prevent huge jumps:
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
if (delta_time < 0.f || 0.25f < delta_time) {
delta_time = 0.f;
} else if (delta_time > 0.1f) {
delta_time = 0.1f;
}
// scale [0, 1] to [-1, 1] to simplify calculations
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
// target is one revolution per second at max speed at 1.0 sensitivity
auto adjusted_delta_value = delta_raw_value * delta_time;
// multiplier / divisor
if (this->getMultiplier() > 1) {
adjusted_delta_value *= this->getMultiplier();
} else if (this->getMultiplier() < -1) {
adjusted_delta_value /= -this->getMultiplier();
}
// sensitivity
if (this->isSensitivitySet()) {
adjusted_delta_value *= this->getSensitivity();
}
// calculate the new absolute value
this->rel_mode_absolute_value += adjusted_delta_value;
}
// update for next poll
this->rel_mode_last_read_time_s = now;
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
return this->rel_mode_absolute_value;
}
float Analog::getDelayedValue(float raw_value) {
const double delay_ms = static_cast<double>(this->getDelayMs());
if (delay_ms == 0.0) {
return raw_value;
}
// always push a new value
const auto now = get_performance_milliseconds();
this->delayed_inputs.emplace(now, raw_value);
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz which is overkill
// (UI only allows for 250ms of delay)
while (this->delayed_inputs.size() > 1024) {
this->delayed_inputs.pop();
}
// pop until we find the oldest value still inside the delay window
while (this->delayed_inputs.size() > 1) {
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
if (delta_t <= delay_ms) {
break;
}
this->delayed_inputs.pop();
}
return this->delayed_inputs.front().value;
}
+46 -18
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <array> #include <array>
#include <string> #include <string>
#include <cmath> #include <cmath>
@@ -13,12 +14,32 @@ namespace rawinput {
class RawInputManager; class RawInputManager;
} }
namespace GameAPI::Analogs {
enum class AnalogType {
// default; values wrap around (below 0 turns into 1, over 1 is 0)
// knobs, turntables
Circular = 0,
// typical joystick that rests at the center and caps at [0, 1]
LinearCentered = 1,
// one-directional value (sliders and instruments like piano/drum velocity)
// starts at 0 and goes up to 1
LinearPositive = 2,
};
}
struct AnalogMovingAverage { struct AnalogMovingAverage {
double time_in_ms; double time_in_ms;
float sine; float sine;
float cosine; float cosine;
}; };
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog { class Analog {
private: private:
std::string name; std::string name;
@@ -31,10 +52,11 @@ private:
float last_state = 0.5f; float last_state = 0.5f;
bool sensitivity_set = false; bool sensitivity_set = false;
bool deadzone_set = false; bool deadzone_set = false;
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
// smoothing function // smoothing function
bool smoothing = false; bool smoothing = false;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history; std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
int vector_history_index = 0; int vector_history_index = 0;
float smoothed_last_state = 0.f; float smoothed_last_state = 0.f;
@@ -48,12 +70,13 @@ private:
unsigned short divisor_region = 0; unsigned short divisor_region = 0;
// relative input mode // relative input mode
float absolute_value_for_rel_mode = 0.5f; float rel_mode_absolute_value = 0.5f;
float rel_mode_last_read_time_s = 0.f;
bool relative_mode = false; bool relative_mode = false;
// circular buffer (delayed input) // delay
int delay_buffer_depth = 0; uint32_t delay_ms = 0;
std::queue<float> delay_buffer; std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads); float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads); float normalizeAngle(float rads);
@@ -66,7 +89,8 @@ public:
float override_state = 0.5f; float override_state = 0.5f;
explicit Analog(std::string name) : name(std::move(name)) { explicit Analog(std::string name) : name(std::move(name)) {
vector_history.fill({0.0, 0.f, 0.f}); };
explicit Analog(std::string name, GameAPI::Analogs::AnalogType type) : name(std::move(name)), type(type) {
}; };
std::string getDisplayString(rawinput::RawInputManager* manager); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -74,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads); float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value); float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value); float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() { inline bool isSet() {
if (this->override_enabled) { if (this->override_enabled) {
@@ -90,8 +116,8 @@ public:
deadzone_mirror = false; deadzone_mirror = false;
setMultiplier(1); setMultiplier(1);
setRelativeMode(false); setRelativeMode(false);
setDelayBufferDepth(0);
setLastState(0.5f); setLastState(0.5f);
setDelayMs(0);
} }
inline void clearBindings() { inline void clearBindings() {
@@ -193,25 +219,27 @@ public:
} }
inline void setRelativeMode(bool relative_mode) { inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
this->relative_mode = relative_mode; this->relative_mode = relative_mode;
this->absolute_value_for_rel_mode = 0.5f; this->rel_mode_absolute_value = 0.5f;
this->rel_mode_last_read_time_s = 0.f;
} }
inline float getAbsoluteValue(float relative_delta) { inline GameAPI::Analogs::AnalogType getType() const {
this->absolute_value_for_rel_mode = return this->type;
normalizeAnalogValue(this->absolute_value_for_rel_mode + relative_delta);
return this->absolute_value_for_rel_mode;
} }
inline int getDelayBufferDepth() const { inline void setType(GameAPI::Analogs::AnalogType type) {
return this->delay_buffer_depth; this->type = type;
} }
inline void setDelayBufferDepth(int depth) { inline uint32_t getDelayMs() const {
this->delay_buffer_depth = depth; return this->delay_ms;
} }
inline std::queue<float> &getDelayBuffer() { inline void setDelayMs(uint32_t delay_ms) {
return this->delay_buffer; this->delay_ms = delay_ms;
} }
}; };
+121 -58
View File
@@ -199,9 +199,19 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
if (vKey < value_states->size()) { if (vKey < value_states->size()) {
auto value = value_states->at(vKey); auto value = value_states->at(vKey);
if (current_button->getAnalogType() == BAT_POSITIVE) { if (current_button->getAnalogType() == BAT_POSITIVE) {
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; float threshold = 0.6f;
if (current_button->getBatThreshold() > 0) {
threshold = std::clamp(current_button->getBatThreshold() / 100.f, 0.01f, 0.99f);
}
state = value > threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else if (current_button->getAnalogType() == BAT_NEGATIVE) { } else if (current_button->getAnalogType() == BAT_NEGATIVE) {
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; float threshold = 0.4f;
if (current_button->getBatThreshold() > 0) {
threshold = std::clamp((100 - current_button->getBatThreshold()) / 100.f, 0.01f, 0.99f);
}
state = value < threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else { } else {
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} }
@@ -669,81 +679,98 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
} }
// deadzone // deadzone
if (analog.isDeadzoneSet()) { // do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
if (analog.isDeadzoneSet() &&
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
value = analog.applyDeadzone(value); value = analog.applyDeadzone(value);
} }
if (analog.isRelativeMode()) { if (analog.getType() == AnalogType::Circular) {
float relative_delta = value - 0.5f;
// built-in scaling to make values reasonable
relative_delta /= 80.f;
// integer multiplier/divisor if (analog.isRelativeMode()) {
const auto mult = analog.getMultiplier(); value = analog.getRelativeModeValue(value);
if (mult < -1) {
relative_delta /= -mult;
} else if (1 < mult) {
relative_delta *= mult;
}
// sensitivity (ranges from 0.0 to 4.0) } else {
if (analog.isSensitivitySet()) { // integer multiplier
relative_delta *= analog.getSensitivity(); value = analog.applyMultiplier(value);
}
// translate relative movement to absolute value // smoothing/sensitivity
value = analog.getAbsoluteValue(relative_delta); if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
} else { // smoothing
// integer multiplier if (analog.getSmoothing()) {
value = analog.applyMultiplier(value);
// smoothing/sensitivity // preserve direction
if (analog.getSmoothing() || analog.isSensitivitySet()) { if (rads >= M_TAU) {
float rads = value * (float) M_TAU; rads -= 0.0001f;
}
// smoothing // calculate angle
if (analog.getSmoothing()) { rads = analog.getSmoothedValue(rads);
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
} }
// calculate angle // sensitivity
rads = analog.getSmoothedValue(rads); if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// apply to value
value = rads * (float) M_1_TAU;
}
}
} else {
// sensitivity
if (analog.isSensitivitySet()) {
// adjust curve
// values < 1.f : less sensitive around neutral
// values > 1.f : more sensitive around neutral
float curve = analog.getSensitivity();
if (curve <= 0.f) {
curve = 0.01f;
}
curve = 1.f / curve;
if (analog.getType() == AnalogType::LinearCentered) {
// convert 0.0..1.0 to -1.0..+1.0
float signed_raw = (value - 0.5f) * 2.0f;
// apply curve
float sign = signed_raw < 0.0f ? -1.0f : 1.0f;
float magnitude = fabsf(signed_raw);
float curved = sign * powf(magnitude, curve);
// convert back to 0.0..1.0
value = curved * 0.5f + 0.5f;
} else {
value = powf(value, curve);
} }
// sensitivity value = std::clamp(value, 0.f, 1.f);
if (analog.isSensitivitySet()) { }
rads = analog.applyAngularSensitivity(rads);
// multiplier / divisor
if (analog.getMultiplier() < -1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f;
} else {
value /= -analog.getMultiplier();
} }
// apply to value value = std::clamp(value, 0.f, 1.f);
value = rads * (float) M_1_TAU;
} else if (analog.getMultiplier() > 1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) * analog.getMultiplier() + 0.5f;
} else {
value *= analog.getMultiplier();
}
value = std::clamp(value, 0.f, 1.f);
} }
} }
// delay // delay
if (0 < analog.getDelayBufferDepth()) { if (analog.getDelayMs() > 0) {
auto& queue = analog.getDelayBuffer(); value = analog.getDelayedValue(value);
// ensure the queue isn't too long; drop old values
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
queue.pop();
}
// always push new value
queue.push(value);
// get a new value to return
if ((int)queue.size() < analog.getDelayBufferDepth()) {
// not enough in the queue, stall for now, shouldn't happen often
value = analog.getLastState();
} else {
value = queue.front();
queue.pop();
}
} }
break; break;
@@ -829,6 +856,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
return sorted; return sorted;
} }
static std::vector<Analog> sortAnalogsWithTypeInternal(
std::vector<Analog> &analogs,
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
std::vector<Analog> sorted;
bool analog_found;
for (auto &a : list) {
analog_found = false;
for (auto &analog : analogs) {
if (a.name == analog.getName()) {
analog_found = true;
analog.setType(a.type);
sorted.push_back(analog);
break;
}
}
if (!analog_found) {
sorted.emplace_back(a.name, a.type);
}
}
return sorted;
}
void GameAPI::Analogs::sortAnalogsWithType(
std::vector<Analog> *analogs,
const std::initializer_list<AnalogWithType> list) {
if (analogs) {
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
}
}
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) { float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
// check override // check override
+8 -7
View File
@@ -85,14 +85,15 @@ namespace GameAPI {
const std::vector<Analog> &analogs, const std::vector<Analog> &analogs,
const std::vector<std::string> &analog_names); const std::vector<std::string> &analog_names);
template<typename T> struct AnalogWithType {
void sortAnalogs(std::vector<Analog> *analogs, T t) { const std::string name;
const std::vector<std::string> analog_names { t }; const AnalogType type;
if (analogs) { AnalogWithType(std::string name, AnalogType type) :
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names); name(std::move(name)), type(type) {}
} };
}
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
template<typename T, typename... Rest> template<typename T, typename... Rest>
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) { void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
+4 -1
View File
@@ -306,7 +306,10 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string vKeyString = fmt::format("{:#x}", vKey); std::string vKeyString = fmt::format("{:#x}", vKey);
// device must be existing // device must be existing
if (this->device_identifier.empty() && vKey == INVALID_VKEY) { if (this->isNaive() && vKey == INVALID_VKEY) {
if (this->getInvert()) {
return "(always on)";
}
return ""; return "";
} }
+10
View File
@@ -56,6 +56,7 @@ private:
ButtonAnalogType analog_type = BAT_NONE; ButtonAnalogType analog_type = BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0; // for positive/negative only, value in percentage, zero is default
bool invert = false; bool invert = false;
bool is_temporary = false; bool is_temporary = false;
@@ -109,6 +110,7 @@ public:
alternatives.clear(); alternatives.clear();
device_identifier = ""; device_identifier = "";
analog_type = BAT_NONE; analog_type = BAT_NONE;
bat_threshold = 0;
} }
std::string getDisplayString(rawinput::RawInputManager* manager); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -169,6 +171,14 @@ public:
this->debounce_down = debounce_time_down; this->debounce_down = debounce_time_down;
} }
inline void setBatThreshold(int bat_threshold) {
this->bat_threshold = bat_threshold;
}
inline int getBatThreshold() const {
return this->bat_threshold;
}
inline bool getInvert() const { inline bool getInvert() const {
return this->invert; return this->invert;
} }
+28 -16
View File
@@ -28,7 +28,7 @@ Config::Config() {
this->status = false; this->status = false;
if (CONFIG_PATH_OVERRIDE.length() > 0) { if (CONFIG_PATH_OVERRIDE.length() > 0) {
this->configLocation = CONFIG_PATH_OVERRIDE; this->configLocation = CONFIG_PATH_OVERRIDE;
log_info("cfg", "using custom config file: {}", this->configLocation.string()); log_info("cfg", "using custom config file: {}", this->configLocation);
} else { } else {
this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml"; this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml";
// avoids logging the expanded appdata path as it contains user name // avoids logging the expanded appdata path as it contains user name
@@ -57,7 +57,7 @@ Config::Config() {
this->firstFillConfigFile(); this->firstFillConfigFile();
break; break;
case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED: case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED:
log_fatal("cfg", "could not open config file: {}", this->configLocation.string()); log_fatal("cfg", "could not open config file: {}", this->configLocation);
break; break;
case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND: case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND:
this->createConfigFile(); this->createConfigFile();
@@ -72,7 +72,7 @@ Config::Config() {
case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN: case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN:
case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT: case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT:
case tinyxml2::XMLError::XML_ERROR_PARSING: case tinyxml2::XMLError::XML_ERROR_PARSING:
log_warning("cfg", "Couldn't read config file: {}", this->configLocation.string()); log_warning("cfg", "Couldn't read config file: {}", this->configLocation);
this->createConfigFile(); this->createConfigFile();
this->firstFillConfigFile(); this->firstFillConfigFile();
break; break;
@@ -162,6 +162,7 @@ bool Config::addGame(Game &game) {
auto analogType = (int) BAT_NONE; auto analogType = (int) BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey); tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
@@ -169,6 +170,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->QueryIntAttribute("analogtype", &analogType); gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up); gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down); gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
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);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) { if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
@@ -180,6 +182,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("debounce_up", debounce_up); gameButtonNode->SetAttribute("debounce_up", debounce_up);
gameButtonNode->SetAttribute("debounce_down", debounce_down); gameButtonNode->SetAttribute("debounce_down", debounce_down);
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);
gameButtonsNode->InsertEndChild(gameButtonNode); gameButtonsNode->InsertEndChild(gameButtonNode);
@@ -188,6 +191,7 @@ bool Config::addGame(Game &game) {
button->setAnalogType((ButtonAnalogType) analogType); button->setAnalogType((ButtonAnalogType) analogType);
button->setDebounceUp(debounce_up); button->setDebounceUp(debounce_up);
button->setDebounceDown(debounce_down); button->setDebounceDown(debounce_down);
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold); button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert); button->setInvert(invert);
if (devid) { if (devid) {
@@ -207,6 +211,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType()); gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
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("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
@@ -248,7 +253,7 @@ bool Config::addGame(Game &game) {
bool smoothing = false; bool smoothing = false;
int multiplier = 1; int multiplier = 1;
bool relative_mode = false; bool relative_mode = false;
int delay_buffer_depth = 0; uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index); tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity); gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone); gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -257,7 +262,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing); gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier); gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode); gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth); gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid"); const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) { if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -273,7 +278,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else { } else {
it.setIndex(static_cast<unsigned short int>(index)); it.setIndex(static_cast<unsigned short int>(index));
@@ -285,7 +290,7 @@ bool Config::addGame(Game &game) {
it.setSmoothing(smoothing); it.setSmoothing(smoothing);
it.setMultiplier(multiplier); it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode); it.setRelativeMode(relative_mode);
it.setDelayBufferDepth(delay_buffer_depth); it.setDelayMs(delay);
} }
} else { } else {
gameAnalogNode = this->configFile.NewElement("analog"); gameAnalogNode = this->configFile.NewElement("analog");
@@ -298,7 +303,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -432,6 +437,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", it.getAnalogType()); gameButtonNode->SetAttribute("analogtype", it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
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("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
@@ -452,7 +458,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -535,6 +541,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType()); gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", button.getBatThreshold());
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());
@@ -551,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", 0); gameButtonNode->SetAttribute("analogtype", 0);
gameButtonNode->SetAttribute("debounce_up", 0.0); gameButtonNode->SetAttribute("debounce_up", 0.0);
gameButtonNode->SetAttribute("debounce_down", 0.0); gameButtonNode->SetAttribute("debounce_down", 0.0);
gameButtonNode->SetAttribute("bat_threshold", 0);
gameButtonNode->SetAttribute("velocity_threshold", 0); gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false); gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", ""); gameButtonNode->SetAttribute("devid", "");
@@ -684,7 +692,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode()); gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else { } else {
gameAnalogNode = this->configFile.NewElement("analog"); gameAnalogNode = this->configFile.NewElement("analog");
@@ -696,7 +704,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode()); gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -946,12 +954,14 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
auto analogType = (int) BAT_NONE; auto analogType = (int) BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
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);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down); gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
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);
const char *devid = gameButtonNode->Attribute("devid"); const char *devid = gameButtonNode->Attribute("devid");
@@ -966,6 +976,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setAnalogType((ButtonAnalogType) analogType); alt.setAnalogType((ButtonAnalogType) analogType);
alt.setDebounceUp(debounce_up); alt.setDebounceUp(debounce_up);
alt.setDebounceDown(debounce_down); alt.setDebounceDown(debounce_down);
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold); alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert); alt.setInvert(invert);
if (devid) { if (devid) {
@@ -984,6 +995,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setAnalogType((ButtonAnalogType) analogType); button.setAnalogType((ButtonAnalogType) analogType);
button.setDebounceUp(debounce_up); button.setDebounceUp(debounce_up);
button.setDebounceDown(debounce_down); button.setDebounceDown(debounce_down);
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold); button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert); button.setInvert(invert);
if (devid) { if (devid) {
@@ -1130,7 +1142,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool smoothing = false; bool smoothing = false;
int multiplier = 1; int multiplier = 1;
bool relative_mode = false; bool relative_mode = false;
int delay_buffer_depth = 0; uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index); gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity); gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone); gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1139,7 +1151,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing); gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier); gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode); gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth); gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid"); const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list // create analog and add to list
@@ -1152,7 +1164,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setSmoothing(smoothing); analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier); analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode); analog.setRelativeMode(relative_mode);
analog.setDelayBufferDepth(delay_buffer_depth); analog.setDelayMs(delay);
if (devid) { if (devid) {
analog.setDeviceIdentifier(devid); analog.setDeviceIdentifier(devid);
} }
@@ -1315,7 +1327,7 @@ void Config::saveConfigFile() {
// create a .tmp file and write to it... // create a .tmp file and write to it...
const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false); const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false);
if (xml_result != tinyxml2::XMLError::XML_SUCCESS) { if (xml_result != tinyxml2::XMLError::XML_SUCCESS) {
log_info("cfg", "failed to write file: {}", this->configLocationTemp.string()); log_info("cfg", "failed to write file: {}", this->configLocationTemp);
return; return;
} }
// copy the .tmp file to the main file... // copy the .tmp file to the main file...
@@ -1325,4 +1337,4 @@ void Config::saveConfigFile() {
} }
// delete the .tmp file (not critical if this fails) // delete the .tmp file (not critical if this fails)
DeleteFileW(this->configLocationTemp.c_str()); DeleteFileW(this->configLocationTemp.c_str());
} }
+7 -2
View File
@@ -24,6 +24,7 @@ namespace overlay::windows {
el->SetAttribute("invert", entry.invert); el->SetAttribute("invert", entry.invert);
el->SetAttribute("debounce_up", entry.debounce_up); el->SetAttribute("debounce_up", entry.debounce_up);
el->SetAttribute("debounce_down", entry.debounce_down); el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold); el->SetAttribute("velocity_threshold", entry.velocity_threshold);
parent->InsertEndChild(el); parent->InsertEndChild(el);
} }
@@ -49,6 +50,10 @@ namespace overlay::windows {
el->QueryIntAttribute("velocity_threshold", &vel); el->QueryIntAttribute("velocity_threshold", &vel);
entry.velocity_threshold = (unsigned short)vel; entry.velocity_threshold = (unsigned short)vel;
int bat = 0;
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
return entry; return entry;
} }
@@ -65,7 +70,7 @@ namespace overlay::windows {
el->SetAttribute("smoothing", analog.smoothing); el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier); el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode); el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay", analog.delay_buffer_depth); el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el); parent->InsertEndChild(el);
} }
@@ -88,7 +93,7 @@ namespace overlay::windows {
el->QueryBoolAttribute("smoothing", &a.smoothing); el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier); el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode); el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryIntAttribute("delay", &a.delay_buffer_depth); el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a; return a;
} }
+5 -5
View File
@@ -20,10 +20,10 @@
</dependentAssembly> </dependentAssembly>
</dependency> </dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges> </requestedPrivileges>
</security> </security>
</trustInfo> </trustInfo>
</assembly> </assembly>
+3 -3
View File
@@ -21,7 +21,7 @@ namespace cfg {
if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) { if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) {
this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value(); this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value();
if (fileutils::file_exists(this->config_path)) { if (fileutils::file_exists(this->config_path)) {
log_info("ScreenResize", "loading config from: {}", this->config_path.string()); log_info("ScreenResize", "loading config from: {}", this->config_path);
file_exists = true; file_exists = true;
} }
} else { } else {
@@ -35,7 +35,7 @@ namespace cfg {
ScreenResize::~ScreenResize() { ScreenResize::~ScreenResize() {
} }
void ScreenResize::config_load() { void ScreenResize::config_load() {
std::string config = fileutils::text_read(this->config_path); std::string config = fileutils::text_read(this->config_path);
if (config.empty()) { if (config.empty()) {
@@ -78,7 +78,7 @@ namespace cfg {
eamuse_get_game(), eamuse_get_game(),
use_game_setting, use_game_setting,
root); root);
load_bool_value(doc, root + "enable_screen_resize", this->enable_screen_resize); load_bool_value(doc, root + "enable_screen_resize", this->enable_screen_resize);
if (this->enable_screen_resize) { if (this->enable_screen_resize) {
log_misc("ScreenResize", "enabled by config file"); log_misc("ScreenResize", "enabled by config file");
+1
View File
@@ -20,6 +20,7 @@ Usage:
#include <memory> #include <memory>
#include <string> #include <string>
#include <map> #include <map>
#include <cstdint>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+1
View File
@@ -39,6 +39,7 @@
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes #define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
#include <algorithm> #include <algorithm>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <functional> #include <functional>
+6 -5
View File
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor"); analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Stick Y" { "Stick X", AnalogType::LinearCentered },
); { "Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers"); analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Joystick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joystick Y", { "Joystick X", AnalogType::LinearCentered },
"Trackball DX", { "Joystick Y", AnalogType::LinearCentered },
"Trackball DY" { "Trackball DX", AnalogType::LinearCentered },
); { "Trackball DY", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+4 -2
View File
@@ -6,6 +6,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/io.h" #include "games/io.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "io.h" #include "io.h"
@@ -112,7 +113,7 @@ namespace games::ccj {
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now(); static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100); static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now(); auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse() & 0x8000; bool pressed = get_async_secondary_mouse();
bool focused = GetForegroundWindow() == hWnd; bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) { if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
@@ -243,6 +244,7 @@ namespace games::ccj {
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);
tbThread = new std::thread([&] { tbThread = new std::thread([&] {
timeutils::PreciseSleepTimer timer;
while (tbThreadRunning) { while (tbThreadRunning) {
if (hWnd && wndProc) { if (hWnd && wndProc) {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle); wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
@@ -251,7 +253,7 @@ namespace games::ccj {
if (!tbThreadRunning) if (!tbThreadRunning)
break; break;
std::this_thread::sleep_for(10ms); timer.sleep(10);
} }
}); });
} }
+58 -11
View File
@@ -23,6 +23,7 @@
#include "p3io/usbmem.h" #include "p3io/usbmem.h"
#include "p4io/p4io.h" #include "p4io/p4io.h"
#include <cstring>
using namespace acioemu; using namespace acioemu;
@@ -53,9 +54,9 @@ namespace games::ddr {
return SendMessage_real(hWnd, Msg, wParam, lParam); return SendMessage_real(hWnd, Msg, wParam, lParam);
} }
bool contains_only_ascii(const std::string& str) { bool contains_only_ascii(const std::wstring& str) {
for (auto c: str) { for (auto c: str) {
if (static_cast<unsigned char>(c) > 127) { if (c > 127) {
return false; return false;
} }
} }
@@ -80,24 +81,24 @@ namespace games::ddr {
} }
if (fileutils::dir_exists(dir)) { if (fileutils::dir_exists(dir)) {
log_info("ddr", "looking for codecs in this directory: {}", dir.string()); log_info("ddr", "looking for codecs in this directory: {}", dir);
} else { } else {
log_info("ddr", "codecs directory not found: {}", dir.string()); log_info("ddr", "codecs directory not found: {}", dir);
return; return;
} }
for (const auto &file : std::filesystem::directory_iterator(dir)) { for (const auto &file : std::filesystem::directory_iterator(dir)) {
const auto &filename = file.path().filename(); const auto &filename = file.path().filename();
const auto extension = strtolower(filename.extension().string()); const auto extension = filename.extension().wstring();
if (extension != ".dll") { if (wcsicmp(extension.c_str(), L".dll") != 0) {
continue; continue;
} }
log_info("ddr", "found DLL: {}, size: {} bytes", filename.string(), file.file_size()); log_info("ddr", "found DLL: {}, size: {} bytes", filename, file.file_size());
if (filename == "k-clvsd.dll" || filename.string().find("xactengine") == 0) { if (filename == "k-clvsd.dll" || filename.wstring().find(L"xactengine") == 0) {
const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\""; const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\"";
const std::string cmd = "regsvr32.exe /s \"" + file.path().string() + "\""; const std::string cmd = fmt::format("regsvr32.exe /s \"{}\"", file.path());
int result = 0; int result = 0;
std::thread t([wcmd, &result]() { std::thread t([wcmd, &result]() {
@@ -111,12 +112,12 @@ namespace games::ddr {
log_warning("ddr", "`{}` failed, returned {}", cmd, result); log_warning("ddr", "`{}` failed, returned {}", cmd, result);
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"DDR codec registration failure", "DDR codec registration failure",
fmt::format(" {} failed to register with error {}", filename.string(), result)}); fmt::format(" {} failed to register with error {}", filename, result)});
} }
static std::once_flag printed; static std::once_flag printed;
std::call_once(printed, [file]() { std::call_once(printed, [file]() {
if (!contains_only_ascii(file.path().string())) { if (!contains_only_ascii(file.path().wstring())) {
log_warning( log_warning(
"ddr", "ddr",
"BAD PATH ERROR\n\n" "BAD PATH ERROR\n\n"
@@ -290,4 +291,50 @@ namespace games::ddr {
// dispose device hook // dispose device hook
devicehook_dispose(); devicehook_dispose();
} }
static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) {
if (!analog.isSet()) {
return;
}
const auto value = GameAPI::Analogs::getState(RI_MGR, analog);
// stepmania linux source:
// https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410
// for example, value cap with range [0, 255]:
// 127 = 0.498, neutral
// 128 = 0.502, both
// apparently some adapters report values like above; this obviously makes a lot of
// assumptions about bit width of the HID value cap and how the adapter reports
// neutral/both values, but this is good enough for parity with stepmania and its many forks
if (0.5001f < value && value < 0.75f) {
less |= true;
more |= true;
} else if (value <= 0.25f) {
less |= true;
} else if (value >= 0.75f) {
more |= true;
}
}
void get_analog_x_axis(int player, bool &left, bool &right) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT);
get_analog_xy_axis(analog, left, right);
}
void get_analog_y_axis(int player, bool &up, bool &down) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN);
get_analog_xy_axis(analog, up, down);
}
} }
+3
View File
@@ -23,4 +23,7 @@ namespace games::ddr {
private: private:
void register_codecs(); void register_codecs();
}; };
void get_analog_x_axis(int player, bool &left, bool &right);
void get_analog_y_axis(int player, bool &up, bool &down);
} }
+44
View File
@@ -35,6 +35,50 @@ std::vector<Button> &games::ddr::get_buttons() {
return analogs; return analogs;
} }
std::string games::ddr::get_buttons_help() {
// keep to max 100 characters wide
return
"For DDR pad arrows, double check that simultaneous Left+Right and Up+Down can be detected.\n"
"You should boot the game, enter test menu, and use Foot Panel Check.\n\n"
"When mapping arrows, try the following in order:\n\n"
" 1. If your controller uses face buttons (A/B/X/Y), bind them here.\n"
" 2. If your controller supports XInput, use that.\n"
" 3. If your controller uses analog axis for arrows, try Analogs tab.\n"
" 4. Otherwise, you will need to use remapping software or get a better adapter."
;
}
std::vector<Analog> &games::ddr::get_analogs() {
static std::vector<Analog> analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered }
});
}
return analogs;
}
std::string games::ddr::get_analogs_help() {
// keep to max 100 characters wide
return
"Only use this if your DDR pad outputs analog axis for arrows.\n\n"
"If the pad uses face buttons (A/B/X/Y), use Buttons tab instead.\n\n"
"Spice will treat values <=25% as Left, >=75% as Right, ~=50% as neutral,\n"
"and value between 50% and 75% as both arrows.\n\n"
"This is the classic Stepmania \"Axis Fix\" which may or may not work with\n"
"your dance pad or your adapter."
;
}
std::vector<Light> &games::ddr::get_lights() { std::vector<Light> &games::ddr::get_lights() {
static std::vector<Light> lights; static std::vector<Light> lights;
+13
View File
@@ -33,6 +33,16 @@ namespace games::ddr {
}; };
} }
// all analogs in correct order
namespace Analogs {
enum {
P1_LEFT_RIGHT,
P1_UP_DOWN,
P2_LEFT_RIGHT,
P2_UP_DOWN
};
}
// all lights in correct order // all lights in correct order
namespace Lights { namespace Lights {
enum { enum {
@@ -177,5 +187,8 @@ namespace games::ddr {
// getters // getters
std::vector<Button> &get_buttons(); std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::string get_analogs_help();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights(); std::vector<Light> &get_lights();
} }
+52 -2
View File
@@ -3,6 +3,7 @@
#include "cfg/api.h" #include "cfg/api.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "../ddr.h" #include "../ddr.h"
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
if (nOutBufferSize >= 4) { if (nOutBufferSize >= 4) {
// cool down // cool down
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// get controls as single variable (4 bytes) // get controls as single variable (4 bytes)
auto &controls = *(uint32_t*) lpOutBuffer; auto &controls = *(uint32_t*) lpOutBuffer;
@@ -498,32 +500,80 @@ int games::ddr::DDRP3IOHandle::device_io(
// shift table // shift table
static size_t shift_table[] = { static size_t shift_table[] = {
30, 28, 29, 8, 9, 10, 11, 12, 24, 25, 14, 15, 16, 17, 18, 19, 20, 26, 27, 22, 23 30, 28, 29, // service, test, coin
8, // p1 start
9, 10, 11, 12, // p1 panel
24, 25, 14, 15, // p1 menu
16, // p2 start
17, 18, 19, 20, // p2 panel
26, 27, 22, 23 // p2 menu
}; };
static size_t button_table[] = { static size_t button_table[] = {
Buttons::SERVICE, Buttons::SERVICE,
Buttons::TEST, Buttons::TEST,
Buttons::COIN_MECH, Buttons::COIN_MECH,
Buttons::P1_START, Buttons::P1_START,
Buttons::P1_PANEL_UP, Buttons::P1_PANEL_UP,
Buttons::P1_PANEL_DOWN, Buttons::P1_PANEL_DOWN,
Buttons::P1_PANEL_LEFT, Buttons::P1_PANEL_LEFT,
Buttons::P1_PANEL_RIGHT, Buttons::P1_PANEL_RIGHT,
Buttons::P1_MENU_UP, Buttons::P1_MENU_UP,
Buttons::P1_MENU_DOWN, Buttons::P1_MENU_DOWN,
Buttons::P1_MENU_LEFT, Buttons::P1_MENU_LEFT,
Buttons::P1_MENU_RIGHT, Buttons::P1_MENU_RIGHT,
Buttons::P2_START, Buttons::P2_START,
Buttons::P2_PANEL_UP, Buttons::P2_PANEL_UP,
Buttons::P2_PANEL_DOWN, Buttons::P2_PANEL_DOWN,
Buttons::P2_PANEL_LEFT, Buttons::P2_PANEL_LEFT,
Buttons::P2_PANEL_RIGHT, Buttons::P2_PANEL_RIGHT,
Buttons::P2_MENU_UP, Buttons::P2_MENU_UP,
Buttons::P2_MENU_DOWN, Buttons::P2_MENU_DOWN,
Buttons::P2_MENU_LEFT, Buttons::P2_MENU_LEFT,
Buttons::P2_MENU_RIGHT, Buttons::P2_MENU_RIGHT,
}; };
// get analogs
struct {
bool up;
bool down;
bool left;
bool right;
} analog_values[2] = {};
games::ddr::get_analog_x_axis(1, analog_values[0].left, analog_values[0].right);
games::ddr::get_analog_y_axis(1, analog_values[0].up, analog_values[0].down);
games::ddr::get_analog_x_axis(2, analog_values[1].left, analog_values[1].right);
games::ddr::get_analog_y_axis(2, analog_values[1].up, analog_values[1].down);
if (analog_values[0].up) {
controls |= 1 << 9;
}
if (analog_values[0].down) {
controls |= 1 << 10;
}
if (analog_values[0].left) {
controls |= 1 << 11;
}
if (analog_values[0].right) {
controls |= 1 << 12;
}
if (analog_values[1].up) {
controls |= 1 << 17;
}
if (analog_values[1].down) {
controls |= 1 << 18;
}
if (analog_values[1].left) {
controls |= 1 << 19;
}
if (analog_values[1].right) {
controls |= 1 << 20;
}
// update states // update states
auto &buttons = get_buttons(); auto &buttons = get_buttons();
size_t count = 0; size_t count = 0;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "../io.h" #include "../io.h"
@@ -255,7 +256,8 @@ namespace games::ddr {
case P4IO_IOCTL_GET_INPUTS: { case P4IO_IOCTL_GET_INPUTS: {
// Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz // Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz
Sleep(4); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(4);
memset(lpOutBuffer, 0, 16); memset(lpOutBuffer, 0, 16);
auto controls = (uint32_t*) lpOutBuffer; auto controls = (uint32_t*) lpOutBuffer;
+3 -1
View File
@@ -7,6 +7,7 @@
#include "games/game.h" #include "games/game.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h" #include "util/memutils.h"
#include "rgb_cam.h" #include "rgb_cam.h"
@@ -221,10 +222,11 @@ namespace games::drs {
void start_touch() { void start_touch() {
std::thread t([] { std::thread t([] {
log_info("drs", "starting touch input thread"); log_info("drs", "starting touch input thread");
timeutils::PreciseSleepTimer timer;
// main loop // main loop
while (TRUE) { while (TRUE) {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); timer.sleep(1);
TOUCH_EVENTS.clear(); TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS); touch_get_events(TOUCH_EVENTS);
+8 -7
View File
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom"); analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad 2", { "Pad 1", AnalogType::LinearPositive },
"Pad 3", { "Pad 2", AnalogType::LinearPositive },
"Pad 4" { "Pad 3", AnalogType::LinearPositive },
); { "Pad 4", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+332 -24
View File
@@ -8,11 +8,14 @@
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h" #include "util/sigscan.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/utils.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
namespace games::gitadora { namespace games::gitadora {
@@ -20,7 +23,6 @@ namespace games::gitadora {
// settings // settings
bool TWOCHANNEL = false; bool TWOCHANNEL = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt; std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool ARENA_SINGLE_WINDOW = false;
bool P1_LEFTY = false; bool P1_LEFTY = false;
bool P2_LEFTY = false; bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
@@ -223,8 +225,7 @@ namespace games::gitadora {
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay // arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable // force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && !cfg::CONFIGURATOR_STANDALONE) {
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250; overlay::UI_SCALE_PERCENT = 250;
} }
@@ -237,9 +238,292 @@ namespace games::gitadora {
} else { } else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy"); log_info("gitadora", "guitar pick SOCD algorithm: legacy");
} }
#if SPICE64 && !SPICE_XP
if (is_arena_model() && !GRAPHICS_WINDOWED && !GRAPHICS_FORCE_SINGLE_ADAPTER) {
const auto &monitors = sysutils::enumerate_monitors();
const size_t active_count = monitors.size();
log_info("gitadora", "arena model: detected {} active monitor(s)", active_count);
if (active_count < 4) {
log_info("gitadora", "arena model: enable single monitor mode due to insufficient monitors");
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
}
}
#endif
} }
} }
#if SPICE64 && !SPICE_XP
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
// HDMI -> main 4k monitor (real primary)
// DP connInstance 0 -> left
// DP connInstance 1 -> right
// DP connInstance 2 -> small (sub/touch)
// ids are negated on the fake monitor headers so they can be distinguished
// from real ones in DisplayConfigGetDeviceInfo.
struct FakeMonitor {
LONG id;
int width;
int height;
int offset_x;
int offset_y;
UINT32 connector_instance;
};
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
// 0 and 1 so the cache is self-consistent.
static void cache_primary_monitor_info() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes_orig(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
}
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
if (QueryDisplayConfig_orig(
QDC_ONLY_ACTIVE_PATHS,
&path_count, all_paths.data(),
&mode_count, all_modes.data(),
nullptr) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
}
all_paths.resize(path_count);
all_modes.resize(mode_count);
// pick the primary monitor: source mode at (0, 0)
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
const auto idx = p.sourceInfo.modeInfoIdx;
return idx < all_modes.size() &&
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
all_modes[idx].sourceMode.position.x == 0 &&
all_modes[idx].sourceMode.position.y == 0;
});
if (primary == all_paths.end()) {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
static
LONG
WINAPI
GetDisplayConfigBufferSizes_hook(
UINT32 Flags,
UINT32 *pNumPathArrayElements,
UINT32 *pNumModeInfoArrayElements)
{
// populate cached primary real monitor on the first call
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = src_idx,
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
},
.targetInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = tgt_idx,
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
.targetAvailable = TRUE,
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
},
.flags = DISPLAYCONFIG_PATH_ACTIVE,
};
modes[src_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
.id = uid,
.adapterId = adapter_id,
.sourceMode = {
.width = static_cast<UINT32>(m.width),
.height = static_cast<UINT32>(m.height),
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
.position = { .x = m.offset_x, .y = m.offset_y },
},
};
modes[tgt_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
.id = uid,
.adapterId = adapter_id,
.targetMode = {},
};
log_misc(
"gitadora",
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
m.id, m.width, m.height, m.offset_x, m.offset_y);
}
static
LONG
WINAPI
QueryDisplayConfig_hook(
UINT32 flags,
UINT32* numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
UINT32* numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
}
return ERROR_SUCCESS;
}
static
LONG
WINAPI
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
{
if (requestPacket == nullptr) {
return DisplayConfigGetDeviceInfo_orig(requestPacket);
}
// handle fake monitors (negative id) directly without calling orig
const auto id = static_cast<int>(requestPacket->id);
if (id < 0) {
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// name must match WrappedIDirect3D9::GetAdapterIdentifier
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
return ERROR_SUCCESS;
}
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
}
}
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = conn_inst;
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
id, targetName->connectorInstance);
return ERROR_SUCCESS;
}
}
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
if (ret != ERROR_SUCCESS ||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
}
return ret;
}
#endif
void GitaDoraGame::attach() { void GitaDoraGame::attach() {
Game::attach(); Game::attach();
@@ -296,38 +580,62 @@ namespace games::gitadora {
// volume change prevention // volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE); hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// touch hook // monitor/touch hooks (windowed or full screen)
if (ARENA_SINGLE_WINDOW) { if (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW) {
// enable touch hook for subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
return;
}
HMODULE gdme_module = libutils::try_module("libgdme.dll"); #if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
}
// window patch // window patch
if (GRAPHICS_WINDOWED && !replace_pattern( if (!is_arena_model()) {
gdme_module, HMODULE gdme_module = libutils::try_module("libgdme.dll");
"754185ED753D8B4118BF0000CB02", if (GRAPHICS_WINDOWED && !replace_pattern(
"9090????9090??????????????12", 0, 0)) gdme_module,
{ "754185ED753D8B4118BF0000CB02",
log_warning("gitadora", "windowed mode failed"); "9090????9090??????????????12", 0, 0)) {
log_warning("gitadora", "windowed mode failed");
}
} }
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
// two channel mod // two channel mod
if (TWOCHANNEL) { if (TWOCHANNEL) {
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module); if (is_arena_model()) {
log_warning("gitadora", "two channel audio (-2ch) is not supported on Arena Model - use a patch instead");
deferredlogs::defer_error_messages({
"two channel audio (-2ch) is not supported on Arena Model - use a patch instead",
});
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) || } else {
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
{ HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
log_warning("gitadora", "two channel mode failed");
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
log_warning("gitadora", "two channel mode failed");
}
} }
} }
-1
View File
@@ -11,7 +11,6 @@ namespace games::gitadora {
// settings // settings
extern bool TWOCHANNEL; extern bool TWOCHANNEL;
extern std::optional<unsigned int> CAB_TYPE; extern std::optional<unsigned int> CAB_TYPE;
extern bool ARENA_SINGLE_WINDOW;
extern bool P1_LEFTY; extern bool P1_LEFTY;
extern bool P2_LEFTY; extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
+12 -10
View File
@@ -86,19 +86,21 @@ std::vector<Button> &games::gitadora::get_buttons() {
std::vector<Analog> &games::gitadora::get_analogs() { std::vector<Analog> &games::gitadora::get_analogs() {
static std::vector<Analog> analogs; static std::vector<Analog> analogs;
using namespace GameAPI::Analogs;
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("GitaDora"); analogs = GameAPI::Analogs::getAnalogs("GitaDora");
GameAPI::Analogs::sortAnalogs(&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Guitar P1 Wail X", {"Guitar P1 Wail X", AnalogType::LinearCentered},
"Guitar P1 Wail Y", {"Guitar P1 Wail Y", AnalogType::LinearCentered},
"Guitar P1 Wail Z", {"Guitar P1 Wail Z", AnalogType::LinearCentered},
"Guitar P1 Knob", {"Guitar P1 Knob", AnalogType::Circular},
"Guitar P2 Wail X", {"Guitar P2 Wail X", AnalogType::LinearCentered},
"Guitar P2 Wail Y", {"Guitar P2 Wail Y", AnalogType::LinearCentered},
"Guitar P2 Wail Z", {"Guitar P2 Wail Z", AnalogType::LinearCentered},
"Guitar P2 Knob" {"Guitar P2 Knob", AnalogType::Circular}
); });
} }
return analogs; return analogs;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "iidx.h" #include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
} }
// sleep - otherwise the IO thread will go too hard on the CPU // sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message // generate message
auto msg = this->create_msg(msg_in, 0x2E); auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "iidx.h" #include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
} }
// sleep - otherwise the IO thread will go too hard on the CPU // sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message // generate message
auto msg = this->create_msg(msg_in, 0x2E); auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -67,6 +67,8 @@ namespace games::iidx {
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;
uint32_t TT_DELAY_P1 = 0;
uint32_t TT_DELAY_P2 = 0;
uint8_t DIGITAL_TT_SENS = 4; uint8_t DIGITAL_TT_SENS = 4;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt;
std::optional<std::string> SCREEN_MODE = std::nullopt; std::optional<std::string> SCREEN_MODE = std::nullopt;
@@ -781,7 +783,7 @@ namespace games::iidx {
} }
// return higher 8 bit // return higher 8 bit
return (uint8_t) (ret_value >> 2); return (uint8_t)(ret_value >> 2);
} }
unsigned char get_slider(uint8_t slider) { unsigned char get_slider(uint8_t slider) {
+11 -10
View File
@@ -63,16 +63,17 @@ std::vector<Analog> &games::iidx::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX"); analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Turntable P1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Turntable P2", { "Turntable P1", AnalogType::Circular },
"VEFX", { "Turntable P2", AnalogType::Circular },
"Low-EQ", { "VEFX", AnalogType::LinearPositive },
"Hi-EQ", { "Low-EQ", AnalogType::LinearPositive },
"Filter", { "Hi-EQ", AnalogType::LinearPositive },
"Play Volume" { "Filter", AnalogType::LinearPositive },
); { "Play Volume", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+3 -1
View File
@@ -3,6 +3,7 @@
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "mf_wrappers.h" #include "mf_wrappers.h"
@@ -399,6 +400,7 @@ namespace games::iidx {
void IIDXLocalCamera::CreateThread() { void IIDXLocalCamera::CreateThread() {
// Create thread // Create thread
m_drawThread = new std::thread([this]() { m_drawThread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0; double accumulator = 0.0;
@@ -412,7 +414,7 @@ namespace games::iidx {
accumulator -= 1.0; accumulator -= 1.0;
floorFrameTimeMicroSec += 1; floorFrameTimeMicroSec += 1;
} }
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec)); timer.sleep(std::chrono::microseconds(floorFrameTimeMicroSec));
} }
}); });
} }
+3 -1
View File
@@ -15,6 +15,7 @@
#include "touch/touch.h" #include "touch/touch.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#define POKE_NATIVE_TOUCH 0 #define POKE_NATIVE_TOUCH 0
@@ -200,6 +201,7 @@ namespace games::iidx::poke {
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log // log
log_info("poke", "enabled"); log_info("poke", "enabled");
@@ -319,7 +321,7 @@ namespace games::iidx::poke {
} }
// slow down // slow down
Sleep(50); timer.sleep(50);
} }
return nullptr; return nullptr;
+3
View File
@@ -92,6 +92,9 @@ namespace games {
games.push_back(ddr); games.push_back(ddr);
buttons.insert({ ddr, ddr::get_buttons() }); buttons.insert({ ddr, ddr::get_buttons() });
lights.insert({ ddr, ddr::get_lights() }); lights.insert({ ddr, ddr::get_lights() });
buttons_help.insert({ ddr, ddr::get_buttons_help() });
analogs_help.insert({ ddr, ddr::get_analogs_help() });
analogs.insert({ ddr, ddr::get_analogs() });
file_hints[ddr].push_back({"ddr.dll"}); file_hints[ddr].push_back({"ddr.dll"});
file_hints[ddr].push_back({"mdxja_945.dll"}); file_hints[ddr].push_back({"mdxja_945.dll"});
file_hints[ddr].push_back({"arkmdxp3.dll"}); file_hints[ddr].push_back({"arkmdxp3.dll"});
+86 -13
View File
@@ -19,7 +19,7 @@
namespace games::jb { namespace games::jb {
// touch stuff // touch stuff
bool TOUCH_LEGACY_BOX = false; JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false; static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false; static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true; static bool IS_PORTRAIT = true;
@@ -78,7 +78,7 @@ namespace games::jb {
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model. // note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear(); TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS); touch_get_points(TOUCH_POINTS);
if (TOUCH_LEGACY_BOX) { if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0; auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -94,6 +94,17 @@ namespace games::jb {
} }
} else { } else {
for (auto &tp : TOUCH_POINTS) { 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 x_relative = tp.x;
int y_relative = tp.y; int y_relative = tp.y;
@@ -110,19 +121,66 @@ namespace games::jb {
y_relative -= 8; y_relative -= 8;
} }
if (x_relative < 0 || y_relative < 0) { 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; continue;
} }
// x_hitbox and y_hitbox is relative to top-left pixel of each button
int x_index = x_relative / JB_BUTTON_HITBOX;
int x_hitbox = x_relative % JB_BUTTON_HITBOX;
int y_index = y_relative / JB_BUTTON_HITBOX;
int y_hitbox = y_relative % JB_BUTTON_HITBOX;
// check if the gap was touched // check if the gap was touched
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) { if (TOUCH_ALGORITHM == AcAccurate) {
continue; // 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 // set the corresponding state
@@ -167,7 +225,7 @@ namespace games::jb {
void JBGame::pre_attach() { void JBGame::pre_attach() {
if (!cfg::CONFIGURATOR_STANDALONE) { if (!cfg::CONFIGURATOR_STANDALONE) {
const auto current_path = std::filesystem::current_path(); const auto current_path = std::filesystem::current_path();
log_misc("jubeat", "current working directory: {}", current_path.string()); log_misc("jubeat", "current working directory: {}", current_path);
if (current_path.parent_path() == current_path.root_path()) { if (current_path.parent_path() == current_path.root_path()) {
log_warning( log_warning(
"jubeat", "jubeat",
@@ -178,7 +236,7 @@ namespace games::jb {
" c:\\jubeat\\contents\\spice.exe <- OK\n\n" " c:\\jubeat\\contents\\spice.exe <- OK\n\n"
"To fix this, create a new directory and move ALL game files there.\n\n" "To fix this, create a new directory and move ALL game files there.\n\n"
"Your current working directory: {}\n", "Your current working directory: {}\n",
current_path.string()); current_path);
log_fatal( log_fatal(
"jubeat", "jubeat",
@@ -193,6 +251,21 @@ namespace games::jb {
// enable touch // enable touch
TOUCH_ENABLE = true; 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");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
+7 -1
View File
@@ -4,8 +4,14 @@
namespace games::jb { namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff // touch stuff
extern bool TOUCH_LEGACY_BOX; extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16]; extern bool TOUCH_STATE[16];
void touch_update(); void touch_update();
+5 -5
View File
@@ -36,11 +36,11 @@ std::vector<Analog> &games::mga::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Metal Gear"); analogs = GameAPI::Analogs::getAnalogs("Metal Gear");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joy X", { "Joy X", AnalogType::LinearCentered },
"Joy Y" { "Joy Y", AnalogType::LinearCentered }
); });
} }
return analogs; return analogs;
+32 -31
View File
@@ -165,37 +165,38 @@ std::vector<Analog> &games::nost::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Nostalgia"); analogs = GameAPI::Analogs::getAnalogs("Nostalgia");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Key 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Key 2", { "Key 1", AnalogType::LinearPositive },
"Key 3", { "Key 2", AnalogType::LinearPositive },
"Key 4", { "Key 3", AnalogType::LinearPositive },
"Key 5", { "Key 4", AnalogType::LinearPositive },
"Key 6", { "Key 5", AnalogType::LinearPositive },
"Key 7", { "Key 6", AnalogType::LinearPositive },
"Key 8", { "Key 7", AnalogType::LinearPositive },
"Key 9", { "Key 8", AnalogType::LinearPositive },
"Key 10", { "Key 9", AnalogType::LinearPositive },
"Key 11", { "Key 10", AnalogType::LinearPositive },
"Key 12", { "Key 11", AnalogType::LinearPositive },
"Key 13", { "Key 12", AnalogType::LinearPositive },
"Key 14", { "Key 13", AnalogType::LinearPositive },
"Key 15", { "Key 14", AnalogType::LinearPositive },
"Key 16", { "Key 15", AnalogType::LinearPositive },
"Key 17", { "Key 16", AnalogType::LinearPositive },
"Key 18", { "Key 17", AnalogType::LinearPositive },
"Key 19", { "Key 18", AnalogType::LinearPositive },
"Key 20", { "Key 19", AnalogType::LinearPositive },
"Key 21", { "Key 20", AnalogType::LinearPositive },
"Key 22", { "Key 21", AnalogType::LinearPositive },
"Key 23", { "Key 22", AnalogType::LinearPositive },
"Key 24", { "Key 23", AnalogType::LinearPositive },
"Key 25", { "Key 24", AnalogType::LinearPositive },
"Key 26", { "Key 25", AnalogType::LinearPositive },
"Key 27", { "Key 26", AnalogType::LinearPositive },
"Key 28" { "Key 27", AnalogType::LinearPositive },
); { "Key 28", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+3 -1
View File
@@ -9,6 +9,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke { namespace games::nost::poke {
@@ -63,6 +64,7 @@ namespace games::nost::poke {
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE); const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6; const int swipe_anim_total_frames = 6;
@@ -178,7 +180,7 @@ namespace games::nost::poke {
} }
// slow down // slow down
Sleep(30); timer.sleep(30);
} }
return nullptr; return nullptr;
+6 -2
View File
@@ -4,6 +4,7 @@
#include "touchpanel.h" #include "touchpanel.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "touch/touch.h" #include "touch/touch.h"
using namespace std::chrono_literals; using namespace std::chrono_literals;
@@ -44,6 +45,9 @@ bool games::onpara::TouchPanelHandle::open(LPCWSTR lpFileName) {
} }
int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
static thread_local timeutils::PreciseSleepTimer timer;
DWORD i; DWORD i;
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer); auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
@@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT
enqueue_packet(report); enqueue_packet(report);
// prevent cpu bullying // prevent cpu bullying
std::this_thread::sleep_for(1ms); timer.sleep(1);
} }
// copy from output queue // copy from output queue
@@ -133,4 +137,4 @@ bool games::onpara::TouchPanelHandle::close() {
log_info("touchpanel", "Closed COM1 (Touch Panel)"); log_info("touchpanel", "Closed COM1 (Touch Panel)");
return true; return true;
} }
+6 -5
View File
@@ -51,11 +51,12 @@ std::vector<Analog> &games::pc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Polaris Chord"); analogs = GameAPI::Analogs::getAnalogs("Polaris Chord");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Fader-L", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Fader-R" { "Fader-L", AnalogType::LinearCentered },
); { "Fader-R", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+1 -1
View File
@@ -261,7 +261,7 @@ namespace games::popn {
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) { } else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket); const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// value must match WrappedIDirect3D9::GetAdapterIdentifier // value must match WrappedIDirect3D9::GetAdapterIdentifier
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE"); wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
} else { } else {
log_fatal( log_fatal(
"popn", "popn",
+27 -13
View File
@@ -4,29 +4,34 @@
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
#include "hooks/audio/backends/dsound/dsound_backend.h" #include "hooks/audio/backends/dsound/dsound_backend.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/precise_timer.h"
#include "util/logging.h"
#include "cfg/configurator.h"
#include "touch.h" #include "touch.h"
namespace games::rb {
uint16_t TOUCH_POLL_RATE = 0;
}
static decltype(SleepEx) *SleepEx_orig; static decltype(SleepEx) *SleepEx_orig;
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAltertable) { static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAlertable) {
/* // increase touch poll rate
* Increase touch poll from ~110 FPS to ~500 FPS
*/
if (dwMilliseconds == 8) { if (dwMilliseconds == 8) {
static bool initialized = false; dwMilliseconds = 1000 / games::rb::TOUCH_POLL_RATE;
if (!initialized) { }
initialized = true;
// if we only sleep for 1ms we also don't need the high priority RB sets // most calls from rb are actually non-alertable
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); if (!bAlertable) {
} static thread_local timeutils::PreciseSleepTimer timer;
dwMilliseconds = 1; timer.sleep(dwMilliseconds);
return 0;
} }
// call original // call original
return SleepEx_orig(dwMilliseconds, bAltertable); return SleepEx_orig(dwMilliseconds, bAlertable);
} }
games::rb::RBGame::RBGame() : Game("Reflec Beat") { games::rb::RBGame::RBGame() : Game("Reflec Beat") {
@@ -35,6 +40,10 @@ games::rb::RBGame::RBGame() : Game("Reflec Beat") {
void games::rb::RBGame::attach() { void games::rb::RBGame::attach() {
Game::attach(); Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) {
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
}
// init stuff // init stuff
devicehook_init(); devicehook_init();
@@ -47,7 +56,12 @@ void games::rb::RBGame::attach() {
} }
// hooks // hooks
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE); if (0 < 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);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
}
} }
void games::rb::RBGame::detach() { void games::rb::RBGame::detach() {
+2
View File
@@ -7,6 +7,8 @@
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;
class RBGame : public games::Game { class RBGame : public games::Game {
public: public:
+14 -9
View File
@@ -13,6 +13,7 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb { namespace games::rb {
uint16_t TOUCH_SCALING = 1000; uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
} }
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) { games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
@@ -160,16 +161,20 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0); const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76); const auto point_y = (int) (y - window_height / 76);
// insert 9 times with offset to double the precision // center
grid_insert(data, point_x, point_y); grid_insert(data, point_x, point_y);
grid_insert(data, point_x - offset_x, point_y);
grid_insert(data, point_x - offset_x, point_y - offset_y); // surrounding points
grid_insert(data, point_x - offset_x, point_y + offset_y); if (games::rb::TOUCH_SIZE == 3) {
grid_insert(data, point_x + offset_x, point_y); grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x + offset_x, point_y + offset_y); grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x + offset_x, point_y - offset_y); grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x, point_y - offset_y); grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x, point_y + offset_y); grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
} }
// copy data to buffer // copy data to buffer
+7 -6
View File
@@ -35,12 +35,13 @@ std::vector<Analog> &games::rf3d::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D"); analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Wheel", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Accelerate", { "Wheel", AnalogType::LinearCentered },
"Brake" { "Accelerate", AnalogType::LinearPositive },
); { "Brake", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -31,13 +31,14 @@ std::vector<Analog> &games::sc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle"); analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Left Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Left Stick Y", { "Left Stick X", AnalogType::LinearCentered },
"Right Stick X", { "Left Stick Y", AnalogType::LinearCentered },
"Right Stick Y" { "Right Stick X", AnalogType::LinearCentered },
); { "Right Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+6 -5
View File
@@ -31,11 +31,12 @@ std::vector<Analog> &games::silentscope::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater"); analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Gun X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Gun Y" { "Gun X", AnalogType::LinearCentered },
); { "Gun Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -50,13 +50,14 @@ std::vector<Analog> &games::we::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Winning Eleven"); analogs = GameAPI::Analogs::getAnalogs("Winning Eleven");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad Stick Left X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad Stick Left Y", { "Pad Stick Left X", AnalogType::LinearCentered },
"Pad Stick Right X", { "Pad Stick Left Y", AnalogType::LinearCentered },
"Pad Stick Right Y" { "Pad Stick Right X", AnalogType::LinearCentered },
); { "Pad Stick Right Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+10 -8
View File
@@ -66,14 +66,16 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
if (FAILED(ret)) { if (FAILED(ret)) {
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) { if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret)); log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret));
} } else if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO)) {
// deal with reverb DMO missing from dsdmo.dll
// Windows 11 KB5052093 issue - reverb DMO went missing from dsdmo.dll (fails with 0x80040154) // it usually fails with 0x80040154 (REGDB_E_CLASSNOTREG), but we have also seen 0x8007007e (ERROR_MOD_NOT_FOUND)
if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO) && ret == (HRESULT)0x80040154) { static std::once_flag printed;
log_warning( std::call_once(printed, [ret]() {
"audio", log_warning(
"CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with 0x80040154, swap with CLSID_DirectSoundWavesReverbDMO " "audio",
"(workaround for Windows 11 KB5052093 issue); REVERB EX replaced with REVERB"); "CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with {}, swap with CLSID_DirectSoundWavesReverbDMO...",
FMT_HRESULT(ret));
});
ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv); ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) { if (FAILED(ret)) {
log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret)); log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret));
@@ -3,6 +3,7 @@
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/backends/wasapi/audio_client.h" #include "hooks/audio/backends/wasapi/audio_client.h"
#include "hooks/audio/backends/wasapi/defs.h" #include "hooks/audio/backends/wasapi/defs.h"
#include "util/precise_timer.h"
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME; static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
@@ -185,6 +186,7 @@ HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppDa
} }
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) { HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
bool written = false; bool written = false;
timeutils::PreciseSleepTimer timer;
// reset the dispatcher event // reset the dispatcher event
ResetEvent(this->dispatcher_event); ResetEvent(this->dispatcher_event);
@@ -209,7 +211,7 @@ HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwF
// avoid pegging the CPU // avoid pegging the CPU
if (!written) { if (!written) {
Sleep(1); timer.sleep(1);
} }
} }
+2 -2
View File
@@ -291,7 +291,7 @@ static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *
std::filesystem::create_directories(mapped_path, err); std::filesystem::create_directories(mapped_path, err);
if (err) { if (err) {
log_warning("hooks::avs", "failed to create '{}': {}", mapped_path.string(), err.message()); log_warning("hooks::avs", "failed to create '{}': {}", mapped_path, err.message());
} else { } else {
// if this is the `e:\`, then create the special directories // if this is the `e:\`, then create the special directories
@@ -305,7 +305,7 @@ static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *
log_misc("hooks::avs", "source directory '{}' remapped to '{}'", log_misc("hooks::avs", "source directory '{}' remapped to '{}'",
fsroot, fsroot,
mapped_path.string()); mapped_path);
} }
new_fs_root = mapped_path.string(); new_fs_root = mapped_path.string();
@@ -465,15 +465,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->GetAdapterCount(); UINT result = pReal->GetAdapterCount();
if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) { if (!FAKE_SUBSCREEN_ADAPTER) {
FAKE_SUBSCREEN_ADAPTER = true; if (games::popn::is_pikapika_model() && result == 1) {
log_info( FAKE_SUBSCREEN_ADAPTER = true;
"graphics::d3d9", log_info(
"GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode"); "graphics::d3d9",
} "GetAdapterCount returned {}, popn needs 2 adapters - enabling fake submonitor mode", result);
return 2;
if (FAKE_SUBSCREEN_ADAPTER) { } else if (games::gitadora::is_arena_model() && !GRAPHICS_WINDOWED && result < 4) {
return 2; FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned {}, gfdm needs 4 adapters - enabling fake submonitor mode", result);
return 4;
}
} }
return result; return result;
@@ -485,12 +491,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier) D3DADAPTER_IDENTIFIER9 *pIdentifier)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
*pIdentifier = {}; *pIdentifier = {};
strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE"); const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
"GetAdapterIdentifier called for fake subscreen adapter 1: {}", "GetAdapterIdentifier called for fake subscreen adapter {}: {}",
Adapter,
pIdentifier->DeviceName); pIdentifier->DeviceName);
return S_OK; return S_OK;
} }
@@ -499,8 +507,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
} }
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter"); log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1; return 1;
} }
@@ -513,8 +521,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode, UINT Mode,
D3DDISPLAYMODE *pMode) D3DDISPLAYMODE *pMode)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter"); log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) { if (Mode == 0 && pMode) {
pMode->Width = 1280; pMode->Width = 1280;
pMode->Height = 800; pMode->Height = 800;
@@ -622,8 +630,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType, D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat) D3DFORMAT CheckFormat)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter"); log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK; return D3D_OK;
} }
@@ -1143,10 +1151,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
// initialize sub screen if IIDX/SDVX requested a multi-head context // initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39"}) && if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
UINT i = 1;
if (games::gitadora::is_arena_model()) {
i = 2;
}
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
} }
} }
@@ -1196,7 +1209,13 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
log_info("graphics::d3d9", "created additional swap chain for windowed mode"); log_info("graphics::d3d9", "created additional swap chain for windowed mode");
} }
} else { } else {
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 2;
}
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) { if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr)); log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
} else { } else {
@@ -1233,7 +1252,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx // iidx/sdvx
int swapchain = 1; int swapchain = 1;
if (games::gitadora::is_arena_model()) { if (games::gitadora::is_arena_model()) {
swapchain = 3; swapchain = 2;
} }
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) { if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
@@ -1452,8 +1471,9 @@ void graphics_d3d9_on_present(
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
const bool is_gfdm_arena = const bool is_gfdm_arena = games::gitadora::is_arena_model() &&
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW; (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW);
const bool is_pika = games::popn::is_pikapika_model(); const bool is_pika = games::popn::is_pikapika_model();
if (is_vm || is_tdj || is_gfdm_arena || is_pika) { if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
graphics_d3d9_ldj_on_present(wrapped_device); graphics_d3d9_ldj_on_present(wrapped_device);
@@ -129,13 +129,17 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release(); this->main_swapchain->Release();
this->main_swapchain = nullptr; this->main_swapchain = nullptr;
} }
if (this->sub_swapchain) { for (auto &sc : this->sub_swapchain) {
this->sub_swapchain->Release(); if (sc) {
this->sub_swapchain = nullptr; sc->Release();
sc = nullptr;
}
} }
if (this->fake_sub_swapchain) { for (auto &sc : this->fake_sub_swapchain) {
this->fake_sub_swapchain->Release(); if (sc) {
this->fake_sub_swapchain = nullptr; sc->Release();
sc = nullptr;
}
} }
if (overlay::ENABLED) { if (overlay::ENABLED) {
@@ -250,27 +254,47 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
WRAP_VERBOSE; WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain); HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) { if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true; create_swap_chain = true;
} else if (games::gitadora::is_arena_model() && } else if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW)) {
pPresentationParameters->BackBufferWidth == 800) { if (pPresentationParameters->BackBufferWidth == 800) {
create_swap_chain = true; // SMALL (subscreen)
create_swap_chain = true;
index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT
create_swap_chain = true;
index = 1;
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2;
}
} else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
}
} }
if (create_swap_chain) { if (create_swap_chain) {
if (SUCCEEDED(hr) && !sub_swapchain) { log_misc(
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain); "graphics::d3d9",
sub_swapchain->should_run_hooks = false; "CreateAdditionalSwapChain called for swap chain {}, creating swap chain",
} else if (FAILED(hr) && !fake_sub_swapchain) { index);
if (SUCCEEDED(hr) && !sub_swapchain[index]) {
sub_swapchain[index] = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain[index]->should_run_hooks = false;
} else if (FAILED(hr) && !fake_sub_swapchain[index]) {
log_warning("graphics::d3d9", log_warning("graphics::d3d9",
"failed to create sub swap chain, hr={}, using fake swap chain", "failed to create sub swap chain, hr={}, using fake swap chain",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
fake_sub_swapchain = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false); fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
fake_sub_swapchain->AddRef(); fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK; return D3D_OK;
} }
@@ -308,22 +332,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true; swap = true;
} }
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() && iSwapChain == 2) {
games::gitadora::ARENA_SINGLE_WINDOW &&
iSwapChain == 3) {
swap = true; swap = true;
} }
if (swap) { if (swap) {
if (sub_swapchain) { if (sub_swapchain[0]) {
sub_swapchain->AddRef(); sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
} else if (fake_sub_swapchain) { } else if (fake_sub_swapchain[0]) {
fake_sub_swapchain->AddRef(); fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
@@ -344,7 +366,7 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
UINT n = pReal->GetNumberOfSwapChains(); UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
n += 1; n += 1;
} }
@@ -204,7 +204,8 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1; std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain = nullptr; WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain = nullptr; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
IDirect3DVertexShader9 *vertex_shader = nullptr; IDirect3DVertexShader9 *vertex_shader = nullptr;
}; };
+138 -26
View File
@@ -59,6 +59,12 @@ static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::optional<graphics_orientation> target_orientation_on_boot;
static UINT target_refresh_rate_on_boot = 0;
static std::optional<std::pair<uint32_t, uint32_t>> target_resolution_on_boot;
static bool monitor_settings_changed = false;
static bool monitor_layout_needs_reset = false;
// flag settings // flag settings
bool GRAPHICS_CAPTURE_CURSOR = false; bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false; bool GRAPHICS_LOG_HRESULT = false;
@@ -104,6 +110,7 @@ static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr;
static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr; static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr;
static decltype(SetWindowPos) *SetWindowPos_orig = nullptr; static decltype(SetWindowPos) *SetWindowPos_orig = nullptr;
static decltype(ShowWindow) *ShowWindow_orig = nullptr; static decltype(ShowWindow) *ShowWindow_orig = nullptr;
static decltype(SetDisplayConfig) *SetDisplayConfig_addr = nullptr;
static void reset_window_hook(HWND hWnd) { static void reset_window_hook(HWND hWnd) {
overlay::destroy(hWnd); overlay::destroy(hWnd);
@@ -157,6 +164,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) { if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used // drop mouse inputs since only wintouches should be used
switch (uMsg) { switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN: case WM_LBUTTONDOWN:
case WM_LBUTTONUP: case WM_LBUTTONUP:
case WM_MBUTTONDOWN: case WM_MBUTTONDOWN:
@@ -232,7 +240,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
switch (uMsg) { switch (uMsg) {
case WM_MOVE: case WM_MOVE:
case WM_SIZE: { case WM_SIZE:
// Update SPICETOUCH space when the main window changes size or moves. // Update SPICETOUCH space when the main window changes size or moves.
// The update happens regardless of whether the "fake" spicetouch window is present or not. // The update happens regardless of whether the "fake" spicetouch window is present or not.
// This allows touches received on subscreen window to be translated correctly. // This allows touches received on subscreen window to be translated correctly.
@@ -249,7 +257,16 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE); SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
} }
} break;
case WM_ACTIVATEAPP:
if (wParam) {
// regained focus
// this *can* get called twice in a row when restoring, but update_monitor_at_runtime
// is idempotent (checks current display settings to see if changes are needed),
// so it shouldn't cause any issues
update_monitor_at_runtime();
}
break;
default: default:
break; break;
} }
@@ -340,16 +357,24 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL"); bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL");
// hide maximize button (prevent misaligned touches) // update style / ex-style
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) { if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
// hide maximize button (prevent misaligned touches)
dwStyle &= ~(WS_MAXIMIZEBOX); dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized // mouse clicks become misaligned when resized
if (is_gfdm_sub_window) {
dwStyle &= ~(WS_SIZEBOX); dwStyle &= ~(WS_SIZEBOX);
}
if ((is_tdj_sub_window || is_sdvx_sub_window || is_popn_sub_window) && GRAPHICS_WSUB_BORDERLESS) { // borderless
dwStyle &= ~(WS_OVERLAPPEDWINDOW); if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
}
// don't show the sub window on task bar / alt-tab targets
if (!GRAPHICS_WINDOWED) {
dwExStyle &= ~(WS_EX_APPWINDOW);
dwExStyle |= WS_EX_TOOLWINDOW;
}
} }
if (is_sdvx_sub_window) { if (is_sdvx_sub_window) {
@@ -396,7 +421,7 @@ 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 (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) { if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOW) {
GFDM_SUBSCREEN_WINDOW = result; GFDM_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
@@ -677,7 +702,7 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOW &&
hWnd != GRAPHICS_HOOKED_WINDOW) { hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
@@ -1142,7 +1167,7 @@ void change_primary_monitor(const std::string &monitor_name) {
const auto DisplayConfigGetDeviceInfo_addr = const auto DisplayConfigGetDeviceInfo_addr =
reinterpret_cast<decltype(DisplayConfigGetDeviceInfo) *>( reinterpret_cast<decltype(DisplayConfigGetDeviceInfo) *>(
GetProcAddress(user32, "DisplayConfigGetDeviceInfo")); GetProcAddress(user32, "DisplayConfigGetDeviceInfo"));
const auto SetDisplayConfig_addr = SetDisplayConfig_addr =
reinterpret_cast<decltype(SetDisplayConfig) *>( reinterpret_cast<decltype(SetDisplayConfig) *>(
GetProcAddress(user32, "SetDisplayConfig")); GetProcAddress(user32, "SetDisplayConfig"));
if (GetDisplayConfigBufferSizes_addr == nullptr || QueryDisplayConfig_addr == nullptr || if (GetDisplayConfigBufferSizes_addr == nullptr || QueryDisplayConfig_addr == nullptr ||
@@ -1257,12 +1282,19 @@ void change_primary_monitor(const std::string &monitor_name) {
log_fatal("graphics", "SetDisplayConfig failed, check -mainmonitor option: {}", status); log_fatal("graphics", "SetDisplayConfig failed, check -mainmonitor option: {}", status);
} }
monitor_layout_needs_reset = true;
// a little extra time for windows to settle and redraw things // a little extra time for windows to settle and redraw things
Sleep(2000); Sleep(2000);
} }
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) { void update_monitor(
// note: all of this is only being done for the primary motnior bool is_boot,
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
// note: all of this is only being done for the primary monitor
// get current settings // get current settings
DEVMODEA dm = {}; DEVMODEA dm = {};
@@ -1330,7 +1362,7 @@ void update_monitor_on_boot(std::optional<graphics_orientation> target_orientati
} }
// update refresh rate // update refresh rate
if (target_refresh_rate > 0) { if (target_refresh_rate > 0 && target_refresh_rate != dm.dmDisplayFrequency) {
log_misc("graphics", log_misc("graphics",
"current refresh rate {} => desired refresh rate {}", "current refresh rate {} => desired refresh rate {}",
dm.dmDisplayFrequency, target_refresh_rate); dm.dmDisplayFrequency, target_refresh_rate);
@@ -1340,27 +1372,107 @@ void update_monitor_on_boot(std::optional<graphics_orientation> target_orientati
needs_update = true; needs_update = true;
} }
if (target_resolution.has_value()) {
if (dm.dmPelsWidth != target_resolution.value().first ||
dm.dmPelsHeight != target_resolution.value().second) {
log_misc(
"graphics",
"current resolution {}, {} => desired resolution {}, {}",
dm.dmPelsWidth, dm.dmPelsHeight,
target_resolution.value().first, target_resolution.value().second);
dm.dmPelsWidth = target_resolution.value().first;
dm.dmPelsHeight = target_resolution.value().second;
needs_update = true;
}
}
if (!needs_update) { if (!needs_update) {
// nothing to do // nothing to do
log_misc("graphics", "display settings are already up to date, no changes needed");
return; return;
} }
const auto result = ChangeDisplaySettings(&dm, CDS_FULLSCREEN); const auto result = ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
if (result != DISP_CHANGE_SUCCESSFUL) { if (result != DISP_CHANGE_SUCCESSFUL) {
log_fatal( if (is_boot) {
"graphics", log_fatal(
"failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options", "graphics",
dm.dmPelsWidth, "failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options",
dm.dmPelsHeight, dm.dmPelsWidth,
dm.dmDisplayFrequency, dm.dmPelsHeight,
result); dm.dmDisplayFrequency,
result);
} else {
log_warning(
"graphics",
"failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options",
dm.dmPelsWidth,
dm.dmPelsHeight,
dm.dmDisplayFrequency,
result);
}
} else { } else {
monitor_settings_changed = true;
// sleep for a little bit after changing monitor settings to delay game launch/resume
Sleep(1000);
log_info("graphics", "display settings updated successfully ({}px x {}px @ {}Hz)", log_info("graphics", "display settings updated successfully ({}px x {}px @ {}Hz)",
dm.dmPelsWidth, dm.dmPelsWidth,
dm.dmPelsHeight, dm.dmPelsHeight,
dm.dmDisplayFrequency); dm.dmDisplayFrequency);
} }
}
// sleep for a little bit after changing monitor settings to delay game launch
Sleep(1000); void update_monitor_on_boot(
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
target_orientation_on_boot = target_orientation;
target_refresh_rate_on_boot = target_refresh_rate;
target_resolution_on_boot = target_resolution;
log_misc("graphics", "applying monitor updates at boot...");
update_monitor(
true,
target_orientation,
target_refresh_rate,
target_resolution);
}
void update_monitor_at_runtime() {
if (!GRAPHICS_WINDOWED && monitor_settings_changed) {
log_misc("graphics", "applying monitor updates at runtime as window regained focus...");
update_monitor(
false,
target_orientation_on_boot,
target_refresh_rate_on_boot,
target_resolution_on_boot);
}
}
void reset_monitor_on_exit() {
// while CDS_FULLSCREEN is *supposed* to be temporary & the OS attempts to
// restore the original settings on exit, it can sometimes fail to do that;
// therefore, we try our best to clean things up on the way out
if (monitor_settings_changed) {
monitor_settings_changed = false;
log_misc("graphics", "resetting monitor settings on exit...");
ChangeDisplaySettingsW(nullptr, 0);
}
// same for this one.
if (monitor_layout_needs_reset) {
monitor_layout_needs_reset = false;
log_misc("graphics", "restoring primary monitor on exit...");
if (SetDisplayConfig_addr != nullptr) {
SetDisplayConfig_addr(
0,
nullptr,
0,
nullptr,
SDC_APPLY | SDC_USE_DATABASE_CURRENT);
}
}
} }
+7 -1
View File
@@ -117,4 +117,10 @@ void graphics_load_windowed_subscreen_parameters();
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height); void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
void change_primary_monitor(const std::string &monitor_name); void change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate); void update_monitor_on_boot(
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution);
void update_monitor_at_runtime();
void reset_monitor_on_exit();
+94
View File
@@ -0,0 +1,94 @@
#include <winsock2.h>
#include <windows.h>
#include <iptypes.h>
#include <iphlpapi.h>
#include <icmpapi.h>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include "util/detour.h"
#include "util/logging.h"
namespace {
#pragma pack(push, 1)
struct IcmpHdr {
uint8_t type;
uint8_t code;
uint16_t checksum_be;
};
#pragma pack(pop)
decltype(IcmpSendEcho) *IcmpSendEcho_orig = nullptr;
DWORD WINAPI IcmpSendEcho_hook(
HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData,
WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize,
DWORD Timeout) {
if (!IcmpSendEcho_orig) {
SetLastError(ERROR_INVALID_FUNCTION);
return 0;
}
DWORD n = IcmpSendEcho_orig(
IcmpHandle,
DestinationAddress,
RequestData,
RequestSize,
RequestOptions,
ReplyBuffer,
ReplySize,
Timeout);
if (n != 0) {
return n;
}
if (ReplyBuffer == nullptr || ReplySize < sizeof(ICMP_ECHO_REPLY) || RequestData == nullptr) {
return 0;
}
if (RequestSize < sizeof(IcmpHdr)) {
return 0;
}
const auto *req = reinterpret_cast<const uint8_t *>(RequestData);
const auto *ih = reinterpret_cast<const IcmpHdr *>(req);
if (ih->type != 8 || ih->code != 0) {
return 0;
}
memset(ReplyBuffer, 0, ReplySize);
auto *reply = reinterpret_cast<ICMP_ECHO_REPLY *>(ReplyBuffer);
const size_t data_off = offsetof(ICMP_ECHO_REPLY, Data);
const size_t avail = ReplySize > data_off ? ReplySize - data_off : 0;
const size_t copy_len = (std::min)(static_cast<size_t>(RequestSize), avail);
reply->Address = DestinationAddress;
reply->Status = IP_SUCCESS;
reply->RoundTripTime = 1;
reply->DataSize = static_cast<USHORT>(copy_len);
if (copy_len > 0) {
memcpy(reinterpret_cast<uint8_t *>(ReplyBuffer) + data_off, RequestData, copy_len);
}
SetLastError(ERROR_SUCCESS);
return 1;
}
} // namespace
void icmphook_iphlpapi_install() {
static bool done = false;
if (done) {
return;
}
done = true;
if (!detour::trampoline_try(
"iphlpapi.dll",
"IcmpSendEcho",
(void *) IcmpSendEcho_hook,
(void **) &IcmpSendEcho_orig)) {
log_warning("network", "ICMP emulation: IcmpSendEcho hook was not installed");
}
}
+692
View File
@@ -0,0 +1,692 @@
#include "icmphook_net.h"
#include "util/detour.h"
#include "util/logging.h"
#include <iphlpapi.h>
#include <mutex>
#include <deque>
#include <vector>
#include <chrono>
#include <cstring>
#include <atomic>
#include <condition_variable>
#include <unordered_map>
void icmphook_iphlpapi_install();
#ifndef SPICE64
using socket_uint = unsigned int;
#else
using socket_uint = unsigned long long;
#endif
namespace icmphook_internal {
constexpr uint32_t k_loopback_host = 0x7f000001u;
#pragma pack(push, 1)
struct IcmpHeaderView {
uint8_t type;
uint8_t code;
uint16_t checksum_be;
};
#pragma pack(pop)
bool icmp_packet_valid(const uint8_t *icmp, size_t len) {
if (len < sizeof(IcmpHeaderView)) {
return false;
}
unsigned sum = 0;
for (size_t i = 0; i + 1 < len; i += 2) {
sum += (static_cast<unsigned>(icmp[i]) << 8) | icmp[i + 1];
sum = (sum >> 16) + (sum & 0xFFFF);
}
if (len & 1) {
sum += static_cast<unsigned>(icmp[len - 1]) << 8;
sum = (sum >> 16) + (sum & 0xFFFF);
}
sum = (sum >> 16) + (sum & 0xFFFF);
return sum == 0xFFFFu || sum == 0;
}
bool icmp_build_echo_reply(std::vector<uint8_t> &out, const uint8_t *req, size_t req_len) {
if (req_len < sizeof(IcmpHeaderView)) {
return false;
}
out.resize(req_len);
memcpy(out.data(), req, req_len);
auto *hdr = reinterpret_cast<IcmpHeaderView *>(out.data());
hdr->type = 0;
hdr->code = 0;
hdr->checksum_be = 0;
unsigned sum = 0;
for (size_t i = 0; i + 1 < out.size(); i += 2) {
sum += (static_cast<unsigned>(out[i]) << 8) | out[i + 1];
sum = (sum >> 16) + (sum & 0xFFFF);
}
if (out.size() & 1) {
sum += static_cast<unsigned>(out[out.size() - 1]) << 8;
sum = (sum >> 16) + (sum & 0xFFFF);
}
sum = (sum >> 16) + (sum & 0xFFFF);
uint16_t csum = static_cast<uint16_t>(~sum);
hdr->checksum_be = htons(csum);
return true;
}
bool icmp_fix_pointer(
const uint8_t *base,
size_t total_len,
const uint8_t **icmp_start,
size_t *icmp_len) {
if (total_len >= 20 && (base[0] >> 4) == 4) {
size_t ihl = (base[0] & 0x0Fu) * 4;
if (ihl < 20 || ihl > total_len || base[9] != IPPROTO_ICMP) {
return false;
}
*icmp_start = base + ihl;
*icmp_len = total_len - ihl;
return true;
}
*icmp_start = base;
*icmp_len = total_len;
return true;
}
bool ipv4_encode_datagram(
std::vector<uint8_t> &out,
uint32_t src_host_order,
uint32_t dst_host_order,
const uint8_t *payload,
size_t payload_len) {
const size_t hdr_len = 20;
if (payload_len > 0xFFFFu - hdr_len) {
return false;
}
const auto total_len = static_cast<uint16_t>(hdr_len + payload_len);
out.resize(hdr_len + payload_len);
uint8_t *p = out.data();
memset(p, 0, hdr_len);
p[0] = 0x45;
p[1] = 0;
p[2] = static_cast<uint8_t>(total_len >> 8);
p[3] = static_cast<uint8_t>(total_len & 0xFF);
p[6] = 0x40;
p[8] = 1;
p[9] = IPPROTO_ICMP;
uint32_t src_wire = htonl(src_host_order);
uint32_t dst_wire = htonl(dst_host_order);
memcpy(p + 12, &src_wire, 4);
memcpy(p + 16, &dst_wire, 4);
unsigned sum = 0;
for (size_t i = 0; i < hdr_len; i += 2) {
sum += (static_cast<unsigned>(p[i]) << 8) | p[i + 1];
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
}
uint16_t hdr_csum = static_cast<uint16_t>(~sum);
p[10] = static_cast<uint8_t>(hdr_csum >> 8);
p[11] = static_cast<uint8_t>(hdr_csum & 0xFF);
memcpy(p + hdr_len, payload, payload_len);
return true;
}
// Cached adapter list. Populated once per process: adapter changes after the
// first ICMP send are intentionally not picked up, in exchange for avoiding
// repeated IP Helper calls on every emulated send.
std::once_flag g_adapters_cache_init;
std::vector<uint8_t> g_adapters_cache;
void fill_adapters_cache() {
std::call_once(g_adapters_cache_init, []() {
ULONG buf_len = 0;
DWORD r = GetAdaptersInfo(nullptr, &buf_len);
if (r != ERROR_BUFFER_OVERFLOW && r != ERROR_SUCCESS) {
return;
}
if (buf_len == 0) {
return;
}
g_adapters_cache.resize(buf_len);
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
if (GetAdaptersInfo(info, &buf_len) != ERROR_SUCCESS) {
g_adapters_cache.clear();
}
});
}
uint32_t local_ipv4_for_peer(
uint32_t peer_host_order,
uint32_t bind_pref_host_order,
bool has_bind) {
if (peer_host_order == k_loopback_host) {
return k_loopback_host;
}
if (has_bind && bind_pref_host_order != 0) {
return bind_pref_host_order;
}
fill_adapters_cache();
if (g_adapters_cache.empty()) {
return peer_host_order;
}
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
for (PIP_ADAPTER_INFO a = info; a != nullptr; a = a->Next) {
uint32_t ip = ntohl(inet_addr(a->IpAddressList.IpAddress.String));
uint32_t mask = ntohl(inet_addr(a->IpAddressList.IpMask.String));
if (mask == 0) {
continue;
}
if ((peer_host_order & mask) == (ip & mask)) {
return ip;
}
}
return peer_host_order;
}
struct EmuSock {
std::deque<std::vector<uint8_t>> queue;
std::condition_variable_any cv;
DWORD rx_timeout_ms = INFINITE;
bool nonblocking = false;
bool has_bind = false;
uint32_t bind_ip_host = 0;
};
std::recursive_mutex g_mu;
std::unordered_map<SOCKET, std::unique_ptr<EmuSock>> g_socks;
std::atomic<uint32_t> g_seq{1};
// Fast-path flag so the bind() hook in networkhook.cpp can short-circuit
// without taking g_mu when the ICMP feature was never enabled.
std::atomic<bool> g_installed{false};
decltype(socket) *socket_orig = nullptr;
decltype(WSASocketW) *WSASocketW_orig = nullptr;
decltype(WSASocketA) *WSASocketA_orig = nullptr;
decltype(closesocket) *closesocket_orig = nullptr;
decltype(bind) *bind_trampoline_orig = nullptr;
decltype(sendto) *sendto_orig = nullptr;
decltype(recvfrom) *recvfrom_orig = nullptr;
decltype(WSASendTo) *WSASendTo_orig = nullptr;
decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr;
decltype(ioctlsocket) *ioctlsocket_orig = nullptr;
decltype(setsockopt) *setsockopt_orig = nullptr;
EmuSock *lookup(SOCKET s) {
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
return nullptr;
}
return it->second.get();
}
SOCKET alloc_icmp_socket() {
std::lock_guard<std::recursive_mutex> lock(g_mu);
for (;;) {
uint32_t x = g_seq.fetch_add(1, std::memory_order_relaxed);
SOCKET s = (SOCKET)(socket_uint)(0xE0000000u | (x & 0x0FFFFFFFu));
if (s == INVALID_SOCKET) {
continue;
}
if (g_socks.find(s) == g_socks.end()) {
g_socks[s] = std::make_unique<EmuSock>();
return s;
}
}
}
void release_icmp_socket(SOCKET s) {
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
it->second->cv.notify_all();
g_socks.erase(it);
}
}
bool process_icmp_send(SOCKET s, const uint8_t *buf, int len, const sockaddr *to, int tolen) {
if (len <= 0 || tolen < (int) sizeof(sockaddr_in) || !buf || !to) {
return true;
}
auto *sin = reinterpret_cast<const sockaddr_in *>(to);
if (sin->sin_family != AF_INET) {
return true;
}
uint32_t peer_host = ntohl(sin->sin_addr.s_addr);
const uint8_t *icmp_ptr = nullptr;
size_t icmp_len = 0;
const auto *raw = reinterpret_cast<const uint8_t *>(buf);
if (!icmp_fix_pointer(raw, static_cast<size_t>(len), &icmp_ptr, &icmp_len)) {
return true;
}
if (!icmp_packet_valid(icmp_ptr, icmp_len)) {
return true;
}
if (icmp_len < sizeof(IcmpHeaderView)) {
return true;
}
const auto *ih = reinterpret_cast<const IcmpHeaderView *>(icmp_ptr);
if (ih->type != 8 || ih->code != 0) {
return true;
}
std::vector<uint8_t> icmp_reply;
if (!icmp_build_echo_reply(icmp_reply, icmp_ptr, icmp_len)) {
return true;
}
std::vector<uint8_t> packet;
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
return true;
}
EmuSock *es = it->second.get();
uint32_t local_host = local_ipv4_for_peer(peer_host, es->bind_ip_host, es->has_bind);
if (!ipv4_encode_datagram(
packet, peer_host, local_host, icmp_reply.data(), icmp_reply.size())) {
return true;
}
es->queue.push_back(std::move(packet));
es->cv.notify_all();
}
return true;
}
SOCKET WINAPI socket_hook(int af, int type, int protocol) {
if (af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw socket {}", (socket_uint) s);
return s;
}
return socket_orig(af, type, protocol);
}
SOCKET WINAPI WSASocketW_hook(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOW lpProtocolInfo,
GROUP g,
DWORD dwFlags) {
const bool is_raw_icmp =
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
if (lpProtocolInfo == nullptr && is_raw_icmp) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw WSASocketW {}", (socket_uint) s);
return s;
}
return WSASocketW_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
}
SOCKET WINAPI WSASocketA_hook(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags) {
const bool is_raw_icmp =
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
if (lpProtocolInfo == nullptr && is_raw_icmp) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw WSASocketA {}", (socket_uint) s);
return s;
}
return WSASocketA_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
}
int WINAPI closesocket_hook(SOCKET s) {
if (lookup(s)) {
release_icmp_socket(s);
return 0;
}
return closesocket_orig(s);
}
int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) {
auto *in = reinterpret_cast<const sockaddr_in *>(name);
if (in->sin_family != AF_INET) {
WSASetLastError(WSAEAFNOSUPPORT);
return SOCKET_ERROR;
}
it->second->has_bind = true;
it->second->bind_ip_host = ntohl(in->sin_addr.s_addr);
return 0;
}
}
return bind_trampoline_orig(s, name, namelen);
}
int WINAPI sendto_hook(
SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen) {
EmuSock *es = lookup(s);
if (!es) {
return sendto_orig(s, buf, len, flags, to, tolen);
}
if (!buf || !to) {
WSASetLastError(WSAEFAULT);
return SOCKET_ERROR;
}
process_icmp_send(s, reinterpret_cast<const uint8_t *>(buf), len, to, tolen);
return len;
}
int WINAPI WSASendTo_hook(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const sockaddr *lpTo,
int iTolen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
EmuSock *es = lookup(s);
if (!es) {
return WSASendTo_orig(
s,
lpBuffers,
dwBufferCount,
lpNumberOfBytesSent,
dwFlags,
lpTo,
iTolen,
lpOverlapped,
lpCompletionRoutine);
}
if (lpOverlapped != nullptr) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesSent) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
int len = (int) lpBuffers[0].len;
process_icmp_send(
s,
reinterpret_cast<const uint8_t *>(lpBuffers[0].buf),
len,
lpTo,
iTolen);
*lpNumberOfBytesSent = static_cast<DWORD>(len);
return 0;
}
// Dequeue one datagram for an emulated ICMP socket into the caller-provided
// buffer. If out_full_dgram_bytes is non-null, it receives the full packet
// size (before truncation to len) so callers can signal WSAEMSGSIZE.
int emu_recv_dequeue(
SOCKET s, char *buf, int len, sockaddr *from, int *fromlen,
size_t *out_full_dgram_bytes) {
if (!buf || len <= 0) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
std::vector<uint8_t> pkt;
{
std::unique_lock<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
auto wait_pred = [&] {
auto j = g_socks.find(s);
return j == g_socks.end() || !j->second->queue.empty();
};
EmuSock *es = it->second.get();
if (es->nonblocking) {
if (es->queue.empty()) {
WSASetLastError(WSAEWOULDBLOCK);
return SOCKET_ERROR;
}
} else if (es->rx_timeout_ms == INFINITE || es->rx_timeout_ms == 0) {
it->second->cv.wait(lock, wait_pred);
} else {
const auto timeout = std::chrono::milliseconds(es->rx_timeout_ms);
if (!it->second->cv.wait_for(lock, timeout, wait_pred)) {
WSASetLastError(WSAETIMEDOUT);
return SOCKET_ERROR;
}
}
it = g_socks.find(s);
if (it == g_socks.end()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
es = it->second.get();
if (es->queue.empty()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
pkt = std::move(es->queue.front());
es->queue.pop_front();
}
if (out_full_dgram_bytes) {
*out_full_dgram_bytes = pkt.size();
}
const int take = (len < (int) pkt.size()) ? len : (int) pkt.size();
memcpy(buf, pkt.data(), static_cast<size_t>(take));
if (from && fromlen && *fromlen >= (int) sizeof(sockaddr_in)) {
auto *out = reinterpret_cast<sockaddr_in *>(from);
memset(out, 0, sizeof(*out));
out->sin_family = AF_INET;
if (pkt.size() >= 20) {
memcpy(&out->sin_addr, pkt.data() + 12, 4);
} else {
out->sin_addr.s_addr = htonl(k_loopback_host);
}
*fromlen = sizeof(sockaddr_in);
}
return take;
}
int WINAPI recvfrom_hook(
SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen) {
if (!lookup(s)) {
return recvfrom_orig(s, buf, len, flags, from, fromlen);
}
return emu_recv_dequeue(s, buf, len, from, fromlen, nullptr);
}
int WINAPI WSARecvFrom_hook(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
sockaddr *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
if (!lookup(s)) {
return WSARecvFrom_orig(
s,
lpBuffers,
dwBufferCount,
lpNumberOfBytesRecvd,
lpFlags,
lpFrom,
lpFromlen,
lpOverlapped,
lpCompletionRoutine);
}
if (lpOverlapped != nullptr) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesRecvd) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
sockaddr_in from_tmp{};
int fromlen_tmp = sizeof(from_tmp);
size_t full_bytes = 0;
int r = emu_recv_dequeue(
s,
lpBuffers[0].buf,
(int) lpBuffers[0].len,
reinterpret_cast<sockaddr *>(&from_tmp),
&fromlen_tmp,
&full_bytes);
if (r == SOCKET_ERROR) {
return SOCKET_ERROR;
}
if (full_bytes > lpBuffers[0].len) {
WSASetLastError(WSAEMSGSIZE);
return SOCKET_ERROR;
}
*lpNumberOfBytesRecvd = static_cast<DWORD>(r);
if (lpFlags) {
*lpFlags = 0;
}
if (lpFrom && lpFromlen && *lpFromlen >= (int) sizeof(sockaddr_in)) {
*reinterpret_cast<sockaddr_in *>(lpFrom) = from_tmp;
*lpFromlen = sizeof(sockaddr_in);
}
return 0;
}
int WINAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
EmuSock *es = it->second.get();
if (argp && static_cast<u_long>(cmd) == FIONBIO) {
es->nonblocking = (*argp != 0);
return 0;
}
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
}
return ioctlsocket_orig(s, cmd, argp);
}
int WINAPI setsockopt_hook(
SOCKET s, int level, int optname, const char *optval, int optlen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
const bool deref_optval = level == SOL_SOCKET && optlen >= (int) sizeof(DWORD);
if (deref_optval && optval) {
EmuSock *es = it->second.get();
auto v = reinterpret_cast<const DWORD *>(optval);
if (optname == SO_RCVTIMEO) {
DWORD ms = *v;
es->rx_timeout_ms = (ms == 0) ? INFINITE : ms;
return 0;
}
if (optname == SO_SNDTIMEO) {
return 0;
}
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (!deref_optval) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
}
}
return setsockopt_orig(s, level, optname, optval, optlen);
}
void install_icmphook_hooks() {
static bool done = false;
if (done) {
return;
}
done = true;
bool ok = true;
ok &= detour::trampoline_try(
"ws2_32.dll", "socket",
(void *) socket_hook, (void **) &socket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASocketW",
(void *) WSASocketW_hook, (void **) &WSASocketW_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASocketA",
(void *) WSASocketA_hook, (void **) &WSASocketA_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "closesocket",
(void *) closesocket_hook, (void **) &closesocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "bind",
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "sendto",
(void *) sendto_hook, (void **) &sendto_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "recvfrom",
(void *) recvfrom_hook, (void **) &recvfrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASendTo",
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSARecvFrom",
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "ioctlsocket",
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "setsockopt",
(void *) setsockopt_hook, (void **) &setsockopt_orig);
if (!ok) {
log_warning(
"network",
"ICMP emulation: one or more ws2_32 hooks failed to install");
} else {
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
}
g_installed.store(true, std::memory_order_release);
}
} // namespace icmphook_internal
bool icmphook_is_emulated_socket(SOCKET s) {
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
return false;
}
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
return icmphook_internal::g_socks.find(s) != icmphook_internal::g_socks.end();
}
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result) {
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
return false;
}
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
if (icmphook_internal::g_socks.find(s) == icmphook_internal::g_socks.end()) {
return false;
}
*out_result = icmphook_internal::bind_hook_ws2(s, name, namelen);
return true;
}
void icmphook_net_init() {
icmphook_internal::install_icmphook_hooks();
icmphook_iphlpapi_install();
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <winsock2.h>
bool icmphook_is_emulated_socket(SOCKET s);
/*!
* Handle bind() for emulated ICMP sockets: records interface address and succeeds without kernel bind.
* Returns true if this socket was handled (caller should return 0).
*/
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result);
void icmphook_net_init();
+6
View File
@@ -14,6 +14,7 @@
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "hooks/icmphook_net.h"
// hooking related stuff // hooking related stuff
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr; static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
@@ -189,6 +190,11 @@ static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen)
#pragma ide diagnostic ignored "OCDFAInspection" #pragma ide diagnostic ignored "OCDFAInspection"
#endif #endif
int icmp_bind_result = 0;
if (icmphook_try_bind(s, name, namelen, &icmp_bind_result)) {
return icmp_bind_result;
}
// cast to sockaddr_in // cast to sockaddr_in
struct sockaddr_in *in_name = (struct sockaddr_in *) name; struct sockaddr_in *in_name = (struct sockaddr_in *) name;
+104 -16
View File
@@ -5,6 +5,7 @@
#include <assert.h> #include <assert.h>
#include <shlwapi.h> #include <shlwapi.h>
#include <windows.h>
#include <cfg/configurator.h> #include <cfg/configurator.h>
#include "api/modules/capture.h" #include "api/modules/capture.h"
@@ -76,6 +77,7 @@
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "hooks/lang.h" #include "hooks/lang.h"
#include "hooks/networkhook.h" #include "hooks/networkhook.h"
#include "hooks/icmphook_net.h"
#include "hooks/unisintrhook.h" #include "hooks/unisintrhook.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "launcher/logger.h" #include "launcher/logger.h"
@@ -98,6 +100,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "rawinput/touch.h" #include "rawinput/touch.h"
#include "reader/reader.h" #include "reader/reader.h"
#include "sdk/sdk.h"
#include "stubs/stubs.h" #include "stubs/stubs.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
@@ -107,6 +110,7 @@
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/peb.h" #include "util/peb.h"
#include "util/precise_timer.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "util/tapeled.h" #include "util/tapeled.h"
@@ -242,6 +246,7 @@ int main_implementation(int argc, char *argv[]) {
bool easrv_smart = false; bool easrv_smart = false;
bool load_stubs = false; bool load_stubs = false;
bool netfix_disable = false; bool netfix_disable = false;
bool icmphook_enable = false;
bool lang_disable = false; bool lang_disable = false;
std::string process_priority_str = "high"; std::string process_priority_str = "high";
bool cardio_enabled = false; bool cardio_enabled = false;
@@ -297,6 +302,26 @@ int main_implementation(int argc, char *argv[]) {
} }
auto &options = *options_ptr; auto &options = *options_ptr;
#if !SPICE_XP
{
// skip elevation only if AutoElevate is explicitly set to "user"
const bool skip_elevation = options[launcher::Options::AutoElevate].is_active() &&
options[launcher::Options::AutoElevate].value_text() == "user";
if (!skip_elevation && !sysutils::is_running_as_admin()) {
log_info("launcher", "relaunching with administrator privileges");
if (sysutils::relaunch_as_admin()) {
exit(0);
} else {
// elevation failed or was denied by the user
log_fatal("launcher", "failed to launch with administrator privileges");
exit(1);
}
}
}
#endif // !SPICE_XP
// check options // check options
// TODO: get rid of some booleans here and make use of the options directly // TODO: get rid of some booleans here and make use of the options directly
if (options[launcher::Options::OpenConfigurator].value_bool()) { if (options[launcher::Options::OpenConfigurator].value_bool()) {
@@ -383,6 +408,16 @@ int main_implementation(int argc, char *argv[]) {
monitor_orientation = ORIENTATION_CW; monitor_orientation = ORIENTATION_CW;
} }
std::optional<std::pair<uint32_t, uint32_t>> monitor_resolution;
if (options[launcher::Options::ChangeResolution].is_active()) {
std::pair<uint32_t, uint32_t> result;
if (parse_width_height(options[launcher::Options::ChangeResolution].value_text(), result)) {
monitor_resolution = result;
} else if (!cfg_run && !cfg::CONFIGURATOR_STANDALONE) {
log_fatal("launcher", "failed to parse -changeres");
}
}
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 // touch emulation gets disabled, might as well turn these on
@@ -609,8 +644,11 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraCabinetType].is_active()) { if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32(); games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
} }
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool() && GRAPHICS_WINDOWED) { if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
games::gitadora::ARENA_SINGLE_WINDOW = true; // for full screen
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
// for windowed
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
} }
if (options[launcher::Options::GitaDoraWailHold].is_active()) { if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32(); socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
@@ -720,6 +758,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::DisableNetworkFixes].value_bool()) { if (options[launcher::Options::DisableNetworkFixes].value_bool()) {
netfix_disable = true; netfix_disable = true;
} }
if (options[launcher::Options::EnableICMPHook].value_bool()) {
icmphook_enable = true;
}
if (options[launcher::Options::DisableACPHook].value_bool()) { if (options[launcher::Options::DisableACPHook].value_bool()) {
lang_disable = true; lang_disable = true;
} }
@@ -877,6 +918,12 @@ int main_implementation(int argc, char *argv[]) {
PIN_MACRO_ENABLED = true; PIN_MACRO_ENABLED = true;
PIN_MACRO_VALUES[1] = options[launcher::Options::Player2PinMacro].value_text(); PIN_MACRO_VALUES[1] = options[launcher::Options::Player2PinMacro].value_text();
} }
if (options[launcher::Options::AutoPinMacroTrigger0].is_active()) {
AUTO_PIN_MACRO_TRIGGER[0] = options[launcher::Options::AutoPinMacroTrigger0].value_text();
}
if (options[launcher::Options::AutoPinMacroTrigger1].is_active()) {
AUTO_PIN_MACRO_TRIGGER[1] = options[launcher::Options::AutoPinMacroTrigger1].value_text();
}
for (auto &reader : options[launcher::Options::ICCAReaderPort].values_text()) { for (auto &reader : options[launcher::Options::ICCAReaderPort].values_text()) {
static int reader_id = 0; static int reader_id = 0;
@@ -1146,11 +1193,32 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) { if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) {
games::jb::TOUCH_LEGACY_BOX = true; games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
} }
if (options[launcher::Options::JubeatTouchAlgo].is_active()) {
if (options[launcher::Options::JubeatTouchAlgo].value_text() == "accurate") {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
} else {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
}
}
// reflec beat touch emulation
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()) {
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
}
if (options[launcher::Options::spice2x_AsioForceUnload].value_bool()) { if (options[launcher::Options::spice2x_AsioForceUnload].value_bool()) {
hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP = true; hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP = true;
} }
@@ -1272,6 +1340,10 @@ int main_implementation(int argc, char *argv[]) {
games::loveplus::CAMERA_ENABLE = true; games::loveplus::CAMERA_ENABLE = true;
} }
if (options[launcher::Options::DisableHighResTimer].value_bool()) {
timeutils::TIMER_HACKS_DISABLE = 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);
@@ -1531,16 +1603,18 @@ int main_implementation(int argc, char *argv[]) {
} }
// early hooks // early hooks
for (auto &hook : early_hooks) { if (!cfg_run) {
log_info("launcher", "loading early hook DLL {}", hook); for (auto &hook : early_hooks) {
libutils::print_dll_info(hook); log_info("launcher", "loading early hook DLL {}", hook);
HMODULE module; libutils::print_dll_info(hook);
if (!(module = libutils::try_library(hook))) { HMODULE module;
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string()); if (!(module = libutils::try_library(hook))) {
deferredlogs::defer_error_messages({ log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
fmt::format("failed to load early hook {}:", hook), deferredlogs::defer_error_messages({
fmt::format(" {}", get_last_error_string()) fmt::format("failed to load early hook {}:", hook),
}); fmt::format(" {}", get_last_error_string())
});
}
} }
} }
@@ -2171,7 +2245,7 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PrimaryMonitor].is_active()) { if (options[launcher::Options::PrimaryMonitor].is_active()) {
change_primary_monitor(options[launcher::Options::PrimaryMonitor].value_text()); change_primary_monitor(options[launcher::Options::PrimaryMonitor].value_text());
} }
update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH); update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH, monitor_resolution);
// initialize raw input // initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>(); RI_MGR = std::make_unique<rawinput::RawInputManager>();
@@ -2224,10 +2298,18 @@ int main_implementation(int argc, char *argv[]) {
hooks::lang::early_init(); hooks::lang::early_init();
} }
// use high resolution timer for this process
timeutils::set_timer_resolution();
// load DLLs // load DLLs
avs::core::load_dll(); avs::core::load_dll();
avs::ea3::load_dll(); avs::ea3::load_dll();
// ICMP emulation (opt-in; before games open raw ICMP sockets)
if (icmphook_enable) {
icmphook_net_init();
}
// net fix // net fix
if (!netfix_disable) { if (!netfix_disable) {
networkhook_init(); networkhook_init();
@@ -2358,15 +2440,16 @@ int main_implementation(int argc, char *argv[]) {
}); });
} else { } else {
bt5api_hook(module); bt5api_hook(module);
sdk::register_sdk_hooks(hook, module);
} }
} }
// layeredfs // layeredfs
if (fileutils::dir_exists("data_mods") && if (fileutils::dir_exists("data_mods") &&
GetModuleHandleA("ifs_hook.dll") == nullptr) { GetModuleHandleA("ifs_hook.dll") == nullptr) {
log_warning("launcher", "data_mods directory found, but ifs_hook.dll is not present; mods will not load"); log_warning("launcher", "data_mods directory found, but ifs_hook.dll is not loaded; mods will not load");
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"data_mods directory was found, but ifs_hook.dll is not present", "data_mods directory was found, but ifs_hook.dll is not loaded",
" your mods will not load", " your mods will not load",
" to fix this, download ifs_layeredfs and add it as a DLL hook (-k)", " to fix this, download ifs_layeredfs and add it as a DLL hook (-k)",
" https://github.com/mon/ifs_layeredfs" " https://github.com/mon/ifs_layeredfs"
@@ -2392,6 +2475,9 @@ int main_implementation(int argc, char *argv[]) {
bt5api_init(); bt5api_init();
} }
// init SDK
sdk::init_sdk_modules();
// API // API
if (api_enable || std::min(api_serial_port.size(), api_serial_baud.size()) > 0) { if (api_enable || std::min(api_serial_port.size(), api_serial_baud.size()) > 0) {
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);
@@ -2499,6 +2585,8 @@ int main_implementation(int argc, char *argv[]) {
bt5api_dispose(); bt5api_dispose();
} }
sdk::fini_sdk_modules();
// stop raw input // stop raw input
RI_MGR.reset(); RI_MGR.reset();
+145 -36
View File
@@ -203,7 +203,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Show Cursor & Touch Emulation Enable", .title = "Show Cursor & Touch Emulation Enable",
.name = "s", .name = "s",
.desc = "Shows the cursor in the game window; also turns on touch emulation. Do not use if you use a real touch screen.", .desc = "Shows the cursor in the game window; also turns on touch emulation. Do not enable this if you have a real touch screen.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Common", .category = "Common",
}, },
@@ -265,7 +265,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Full Screen Orientation Swap (EXPERIMENTAL)", .title = "Full Screen Orientation Swap (EXPERIMENTAL)",
.name = "forceresswap", .name = "forceresswap",
.desc = .desc =
"Allows you to play portrait games in in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n" "Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n" "Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n" "Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues " "WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
@@ -312,7 +312,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-dx9on12", .name = "sp2x-dx9on12",
.display_name = "dx9on12", .display_name = "dx9on12",
.aliases= "dx9on12", .aliases= "dx9on12",
.desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect games on that don't use DX9. Can cause some games to crash.\n\n" .desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect on games that don't use DX9. Can cause some games to crash.\n\n"
"Default: auto (use DX9 for most games, but turn on 9on12 for games that require it on non-NVIDIA GPUs).", "Default: auto (use DX9 for most games, but turn on 9on12 for games that require it on non-NVIDIA GPUs).",
.type = OptionType::Enum, .type = OptionType::Enum,
.category = "Graphics (Common)", .category = "Graphics (Common)",
@@ -1038,9 +1038,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
}, },
{ {
.title = "GitaDora Two Channel Audio", .title = "GitaDora Two Channel Audio (DX/SD only)",
.name = "2ch", .name = "2ch",
.desc = "Attempt to reduce audio channels down to just two channels.", .desc = "Attempt to reduce audio channels down to just two channels. "
"This option does nothing for Arena Model (GW Delta and above); need a patch for that.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "GitaDora", .game_name = "GitaDora",
.category = "Game Options", .category = "Game Options",
@@ -1056,9 +1057,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// GitaDoraArenaSingleWindow // GitaDoraArenaSingleWindow
.title = "GitaDora Arena Single Window (EXPERIMENTAL)", .title = "GitaDora Arena Disable Subscreens (EXPERIMENTAL)",
.name = "gdaonewindow", .name = "gdaonewindow",
.desc = "For Arena Model, when combined with windowed mode, only show the main game window; enables subscreen overlay.", .desc = "For Arena Model:\n\n"
"Windowed mode: instead of 4 windows, create 1 window.\n\n"
"Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n"
"To access the subscreen, use the subscreen overlay.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "GitaDora", .game_name = "GitaDora",
.category = "Game Options", .category = "Game Options",
@@ -1069,8 +1073,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "gdlefty", .name = "gdlefty",
.desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n" .desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n"
"Without this option, enabling LEFT in the game option will continuously trigger UP wail.\n\n" "Without this option, enabling LEFT in the game option will continuously trigger UP wail.\n\n"
"Has no effect if you are using analog bindings for X/Y axis; expectation is that your controller " "Has no effect if you are using analog bindings for X/Y axis; most controllers do not handle this correctly.\n\n"
"handles this correctly (most do not, however).\n\n"
"As always, remember to restart the game after changing options. If you need to change in the game, " "As always, remember to restart the game after changing options. If you need to change in the game, "
"use the I/O panel overlay window.", "use the I/O panel overlay window.",
.type = OptionType::Enum, .type = OptionType::Enum,
@@ -1268,7 +1271,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Force Load LovePlus Module", .title = "Force Load LovePlus Module",
.name = "loveplus", .name = "loveplus",
.desc = "manually enable LovePlus module.", .desc = "Manually enable LovePlus module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "LovePlus", .game_name = "LovePlus",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1276,7 +1279,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Force Load Charge Machine Module", .title = "Force Load Charge Machine Module",
.name = "pcm", .name = "pcm",
.desc = "manually enable Charge Machine module.", .desc = "Manually enable Charge Machine module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Charge Machine", .game_name = "Charge Machine",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1284,7 +1287,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Force Load Ongaku Paradise Module", .title = "Force Load Ongaku Paradise Module",
.name = "onpara", .name = "onpara",
.desc = "manually enable Ongaku Paradise module.", .desc = "Manually enable Ongaku Paradise module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Ongaku Paradise", .game_name = "Ongaku Paradise",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1292,7 +1295,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Force Load Busou Shinki Module", .title = "Force Load Busou Shinki Module",
.name = "busou", .name = "busou",
.desc = "manually enable Busou Shinki module.", .desc = "Manually enable Busou Shinki module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Busou Shinki: Armored Princess Battle Conductor", .game_name = "Busou Shinki: Armored Princess Battle Conductor",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1301,7 +1304,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadCCJModule // LoadCCJModule
.title = "Force Load Chase Chase Jokers Module", .title = "Force Load Chase Chase Jokers Module",
.name = "ccj", .name = "ccj",
.desc = "manually enable Chase Chase Jokers module.", .desc = "Manually enable Chase Chase Jokers module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Chase Chase Jokers", .game_name = "Chase Chase Jokers",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1310,7 +1313,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadQKSModule // LoadQKSModule
.title = "Force Load QuizKnock STADIUM Module", .title = "Force Load QuizKnock STADIUM Module",
.name = "qks", .name = "qks",
.desc = "manually enable QuizKnock STADIUM module", .desc = "Manually enable QuizKnock STADIUM module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "QuizKnock STADIUM", .game_name = "QuizKnock STADIUM",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1319,7 +1322,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadMFGModule // LoadMFGModule
.title = "Force Load Mahjong Fight Girl Module", .title = "Force Load Mahjong Fight Girl Module",
.name = "mfg", .name = "mfg",
.desc = "manually enable Mahjong Fight Girl module.", .desc = "Manually enable Mahjong Fight Girl module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Mahjong Fight Girl", .game_name = "Mahjong Fight Girl",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1328,7 +1331,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadPCModule // LoadPCModule
.title = "Force Load Polaris Chord Module", .title = "Force Load Polaris Chord Module",
.name = "pc", .name = "pc",
.desc = "manually enable Polaris Chord module.", .desc = "Manually enable Polaris Chord module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Polaris Chord", .game_name = "Polaris Chord",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1337,7 +1340,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadMusecaModule // LoadMusecaModule
.title = "Force Load Museca Module", .title = "Force Load Museca Module",
.name = "museca", .name = "museca",
.desc = "manually enable Museca module.", .desc = "Manually enable Museca module.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Museca", .game_name = "Museca",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
@@ -1461,7 +1464,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "API Verbose Logging", .title = "API Verbose Logging",
.name = "apilogging", .name = "apilogging",
.desc = "verbose logging of API activity.", .desc = "Verbose logging of API activity.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "API (Dev)", .category = "API (Dev)",
}, },
@@ -1584,8 +1587,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Force Touch Emulation", .title = "Force Touch Emulation",
.name = "touchemuforce", .name = "touchemuforce",
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. " .desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
"This is automatically turned on when needed, so it is not typically required to " "This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
"turn it on manually. Do not enable this unless you have trouble.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
@@ -1688,7 +1690,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"legacy: Preserve buggy old behavior (Default)\n\n" "legacy: Preserve buggy old behavior (Default)\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 upto 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. "
"For example, abcd56780123 is converted to e00401abcd567801 on card in. This results in different card numbers so " "For example, abcd56780123 is converted to e00401abcd567801 on card in. This results in different card numbers so "
"be careful when connecting to servers!", "be careful when connecting to servers!",
.type = OptionType::Enum, .type = OptionType::Enum,
@@ -1719,7 +1721,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)", .title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)",
.name = "realtime", .name = "realtime",
.desc = "Sets the process priority to realtime, can help with odd lag spikes.", .desc = "Sets the process priority to realtime; can help with odd lag spikes.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true, .hidden = true,
.category = "Performance", .category = "Performance",
@@ -1730,7 +1732,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-processpriority", .name = "sp2x-processpriority",
.display_name = "processpriority", .display_name = "processpriority",
.aliases= "processpriority", .aliases= "processpriority",
.desc = "Sets the process priority, can help with odd lag spikes. Default: high.", .desc = "Sets the process priority, which can help with odd lag spikes. Default: high.",
.type = OptionType::Enum, .type = OptionType::Enum,
.category = "Performance", .category = "Performance",
.elements = { .elements = {
@@ -1807,7 +1809,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n" .desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n"
"Default: off (allow Spice to hook IMMDeviceEnumerator for and optionally let " "Default: off (allow Spice to hook IMMDeviceEnumerator for and optionally let "
"you use a different backend instead of exclusive WASAPI).\n\n" "you use a different backend instead of exclusive WASAPI).\n\n"
"Check this if you want games to natively access your audio device.", "Check this to allow games to natively access your audio device.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Audio (Hacks)", .category = "Audio (Hacks)",
}, },
@@ -1819,7 +1821,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.aliases= "volumehookdisable", .aliases= "volumehookdisable",
.desc = "Disables audio volume hook.\n\n" .desc = "Disables audio volume hook.\n\n"
"Default: off (prevent games from changing audio volume by hooking IAudioEndpointVolume).\n\n" "Default: off (prevent games from changing audio volume by hooking IAudioEndpointVolume).\n\n"
"Check this if you want games to freely change your volume.", "Check this to allow games to freely change your volume.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Audio (Hacks)", .category = "Audio (Hacks)",
}, },
@@ -1920,6 +1922,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"3", "Landscape, Flipped"} {"3", "Landscape, Flipped"}
}, },
}, },
{
// ChangeResolution
.title = "Change Monitor Resolution",
.name = "changeres",
.desc = "Changes monitor resolution before booting the game.",
.type = OptionType::Text,
.setting_name = "1280,720",
.category = "Monitor"
},
{ {
.title = "AVS Log Level", .title = "AVS Log Level",
.name = "loglevel", .name = "loglevel",
@@ -1952,7 +1963,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "Blocking Logger", .title = "Blocking Logger",
.name = "logblock", .name = "logblock",
.desc = "Slower but safer logging used for debugging.", .desc = "Slower but safer logging for debugging.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Debug Log", .category = "Debug Log",
}, },
@@ -2279,19 +2290,35 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_JubeatLegacyTouch // spice2x_JubeatLegacyTouch
.title = "JB Legacy Touch Targets", .title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)",
.name = "sp2x-jubeatlegacytouch", .name = "sp2x-jubeatlegacytouch",
.display_name = "jubeatlegacytouch", .display_name = "jubeatlegacytouch",
.aliases= "jubeatlegacytouch", .aliases= "jubeatlegacytouch",
.desc = "For touch screen players - use the legacy & less accurate grid-based layout for touch recognition, " .desc = "Deprecated - use -jubeattouchalgo instead.",
"instead of the new & more accurate touch targets. Default: off.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true,
.game_name = "Jubeat", .game_name = "Jubeat",
.category = "Game Options", .category = "Game Options",
}, },
{
.title = "JB Touch Algorithm",
.name = "jubeattouchalgo",
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
.type = OptionType::Enum,
.game_name = "Jubeat",
.category = "Game Options",
.elements = {
{"legacy", ""},
{"improved", ""},
{"accurate", ""},
},
},
{ {
// spice2x_RBTouchScale // spice2x_RBTouchScale
.title = "RB Scale Touch Input", .title = "RB Touch Emulation Scale",
.name = "sp2x-rbscaletouch", .name = "sp2x-rbscaletouch",
.display_name = "rbscaletouch", .display_name = "rbscaletouch",
.aliases= "rbscaletouch", .aliases= "rbscaletouch",
@@ -2301,6 +2328,31 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "Reflec Beat", .game_name = "Reflec Beat",
.category = "Game Options", .category = "Game Options",
}, },
{
// RBTouchSize
.title = "RB Touch Emulation Size",
.name = "rbtouchsize",
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
.type = OptionType::Enum,
.game_name = "Reflec Beat",
.category = "Game Options",
.elements = {
{"1", "1x1"},
{"3", "3x3"},
},
},
{
// RBTouchPollRate
.title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
.type = OptionType::Integer,
.setting_name = "250",
.game_name = "Reflec Beat",
.category = "Game Options",
},
{ {
// spice2x_AsioForceUnload // spice2x_AsioForceUnload
.title = "ASIO Force Unload On Stop", .title = "ASIO Force Unload On Stop",
@@ -2438,6 +2490,28 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"both", ""}, {"both", ""},
}, },
}, },
{
// AutoPinMacroTrigger0
.title = "Auto PIN Macro Trigger Log String (P1)",
.name = "autopinmacrotrigger0",
.desc =
"Substring matched against game log output to detect when Player 1's PIN entry "
"screen is ready. When the substring appears in any log line, the PIN macro is "
"typed for Player 1 only. Leave blank to disable auto-trigger for P1.",
.type = OptionType::Text,
.category = "Network (Advanced)",
},
{
// AutoPinMacroTrigger1
.title = "Auto PIN Macro Trigger Log String (P2)",
.name = "autopinmacrotrigger1",
.desc =
"Substring matched against game log output to detect when Player 2's PIN entry "
"screen is ready. When the substring appears in any log line, the PIN macro is "
"typed for Player 2 only. Leave blank to disable auto-trigger for P2.",
.type = OptionType::Text,
.category = "Network (Advanced)",
},
{ {
// spice2x_LowLatencySharedAudio // spice2x_LowLatencySharedAudio
.title = "Low Latency Shared Audio", .title = "Low Latency Shared Audio",
@@ -2477,7 +2551,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.aliases= "nonvapi", .aliases= "nonvapi",
.desc = "Completely block the game from accessing NVAPI. Can be used if NVAPI is returning undesirable " .desc = "Completely block the game from accessing NVAPI. Can be used if NVAPI is returning undesirable "
"results to the game (e.g., wrong values from NvDisplayConfig for SDVX). You may need to apply additional " "results to the game (e.g., wrong values from NvDisplayConfig for SDVX). You may need to apply additional "
"hex edits in order to boot the game correctly.", "hex edits to boot the game correctly.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Graphics (Common)", .category = "Graphics (Common)",
}, },
@@ -2525,7 +2599,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-smx-stage", .name = "sp2x-smx-stage",
.display_name = "smxstage", .display_name = "smxstage",
.aliases= "smxstage", .aliases= "smxstage",
.desc = "StepmaniaX stage will show up as a device that can recieve lighting output. " .desc = "StepmaniaX stage will show up as a device that can receive lighting output. "
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.", "For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "I/O Options", .category = "I/O Options",
@@ -2536,7 +2610,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-smx-dedicab", .name = "sp2x-smx-dedicab",
.display_name = "smxdedicab", .display_name = "smxdedicab",
.aliases = "smxdedicab", .aliases = "smxdedicab",
.desc = "StepManiaX Dedicated Cabinet will show up as a device that can recieve lighting output. " .desc = "StepManiaX Dedicated Cabinet will show up as a device that can receive lighting output. "
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.", "For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "I/O Options" .category = "I/O Options"
@@ -2640,7 +2714,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"Enables emulation of touch gestures to make it easier to navigate the game without a touchscreen.\n\n" "Enables emulation of touch gestures to make it easier to navigate the game without a touchscreen.\n\n"
"This enables two things:\n" "This enables two things:\n"
" 1. Swipe/Touch bindings in Buttons tab (Swipe Next Page, etc)\n" " 1. Swipe/Touch bindings in Buttons tab (Swipe Next Page, etc)\n"
" 2. PIN pad bindings (make sure Unscramble Keypad patch is applied)).", " 2. PIN pad bindings (make sure Unscramble Keypad patch is applied).",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Nostalgia", .game_name = "Nostalgia",
.category = "Game Options", .category = "Game Options",
@@ -2769,7 +2843,42 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Otoca D'or", .game_name = "Otoca D'or",
.category = "Game Options (Peripherals)", .category = "Game Options (Peripherals)",
} },
{
// DisableHighResTimer
.title = "Use Legacy Timers",
.name = "notimerhacks",
.desc = "Disables high resolution timers, reverting to legacy behavior. "
"Recommended that you leave this OFF for optimal input latency, "
"unless you're on a resource-constrained system (e.g., old cabinet PC)",
.type = OptionType::Bool,
.category = "Development"
},
{
// EnableICMPHook
.title = "Enable ICMP Emulation",
.name = "icmphook",
.desc = "Emulate keepalive ping replies in user mode, so the game does not need to "
"open privileged raw ICMP sockets.",
.type = OptionType::Bool,
.category = "Network (Development)",
},
{
// AutoElevate
.title = "Run as",
.name = "runas",
.desc = "Controls whether spice will automatically re-launch with administrator privileges at startup.\n\n"
"admin (default): automatically re-launch with administrator privileges, "
"needed for most games to function properly\n\n"
"user: skip elevation, run with current user privileges; game may fail to launch, "
"crash, or critical features may silently fail; use at your own risk.",
.type = OptionType::Enum,
.category = "Development",
.elements = {
{"admin", ""},
{"user", ""},
},
},
}; };
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) { const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+9
View File
@@ -199,6 +199,7 @@ namespace launcher {
LoadStubs, LoadStubs,
AdjustOrientation, AdjustOrientation,
spice2x_AutoOrientation, spice2x_AutoOrientation,
ChangeResolution,
LogLevel, LogLevel,
EAAutomap, EAAutomap,
EANetdump, EANetdump,
@@ -239,7 +240,10 @@ namespace launcher {
IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenBorderless,
IIDXWindowedSubscreenAlwaysOnTop, IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch, spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
spice2x_RBTouchScale, spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
spice2x_AsioForceUnload, spice2x_AsioForceUnload,
spice2x_IIDXNoESpec, spice2x_IIDXNoESpec,
spice2x_IIDXWindowedTDJ, spice2x_IIDXWindowedTDJ,
@@ -251,6 +255,8 @@ namespace launcher {
IIDXSubMonitorOverride, IIDXSubMonitorOverride,
spice2x_IIDXEmulateSubscreenKeypadTouch, spice2x_IIDXEmulateSubscreenKeypadTouch,
spice2x_AutoCard, spice2x_AutoCard,
AutoPinMacroTrigger0,
AutoPinMacroTrigger1,
spice2x_LowLatencySharedAudio, spice2x_LowLatencySharedAudio,
spice2x_TapeLedAlgorithm, spice2x_TapeLedAlgorithm,
spice2x_NoNVAPI, spice2x_NoNVAPI,
@@ -279,6 +285,9 @@ namespace launcher {
LovePlusPrinterJPGQuality, LovePlusPrinterJPGQuality,
OptionConflictResolution, OptionConflictResolution,
OtocaCamHook, OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
AutoElevate,
}; };
enum class OptionsCategory { enum class OptionsCategory {
+7
View File
@@ -7,12 +7,14 @@
#include "easrv/easrv.h" #include "easrv/easrv.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/graphics/graphics.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/logging.h" #include "util/logging.h"
#include "launcher.h" #include "launcher.h"
#include "logger.h" #include "logger.h"
#include "nvapi/nvapi.h" #include "nvapi/nvapi.h"
#include "sdk/sdk.h"
namespace launcher { namespace launcher {
@@ -22,6 +24,11 @@ namespace launcher {
// therefore, subsystems need to be guarded against multiple unload attempts // therefore, subsystems need to be guarded against multiple unload attempts
log_info("launcher", "stopping subsystems"); log_info("launcher", "stopping subsystems");
sdk::fini_sdk_modules();
// reset monitor settings
reset_monitor_on_exit();
// before shutting down logger, dump any deferred log messages // before shutting down logger, dump any deferred log messages
deferredlogs::dump_to_logger(); deferredlogs::dump_to_logger();
+3 -1
View File
@@ -67,7 +67,9 @@ namespace superexit {
} }
} }
bool async_key_exit = GetAsyncKeyState(VK_MENU) && GetAsyncKeyState(VK_F4); bool async_key_exit =
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
bool overlay_exit = false; bool overlay_exit = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game()); auto buttons = games::get_buttons_overlay(eamuse_get_game());
+61 -15
View File
@@ -1,5 +1,6 @@
#include "eamuse.h" #include "eamuse.h"
#include <atomic>
#include <fstream> #include <fstream>
#include <thread> #include <thread>
@@ -9,6 +10,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
@@ -43,6 +45,9 @@ static uint8_t AUTO_INSERT_CARD_CACHED_DATA[2][8];
// pin macro // pin macro
bool PIN_MACRO_ENABLED = false; bool PIN_MACRO_ENABLED = false;
std::string PIN_MACRO_VALUES[2] = {"", ""}; std::string PIN_MACRO_VALUES[2] = {"", ""};
std::string AUTO_PIN_MACRO_TRIGGER[2];
static std::atomic_bool AUTO_PIN_MACRO_REQUEST[2] {false, false};
static bool AUTO_PIN_MACRO_PLAYER_ACTIVE[2] = {false, false};
static std::thread *PIN_MACRO_THREAD = nullptr; static std::thread *PIN_MACRO_THREAD = nullptr;
static bool PIN_MACRO_THREAD_ACTIVE = false; static bool PIN_MACRO_THREAD_ACTIVE = false;
static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = { static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
@@ -50,6 +55,18 @@ static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
games::OverlayButtons::TriggerPinMacroP2 games::OverlayButtons::TriggerPinMacroP2
}; };
static bool pin_macro_log_hook(void *, const std::string &data, logger::Style, std::string &) {
for (int unit = 0; unit < 2; unit++) {
if (!AUTO_PIN_MACRO_PLAYER_ACTIVE[unit]) {
continue;
}
if (data.find(AUTO_PIN_MACRO_TRIGGER[unit]) != std::string::npos) {
AUTO_PIN_MACRO_REQUEST[unit].store(true);
}
}
return false;
}
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) { bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
// get unit index // get unit index
@@ -87,7 +104,7 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK); std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
const auto card_override = CARD_OVERRIDES[index]; const auto card_override = CARD_OVERRIDES[index];
lock.unlock(); lock.unlock();
// Check if card overrides are present // Check if card overrides are present
if (!card_override.empty()) { if (!card_override.empty()) {
@@ -131,7 +148,7 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
// open file // open file
std::ifstream f(path); std::ifstream f(path);
if (!f) { if (!f) {
log_warning("eamuse", "{} can not be opened!", path.string()); log_warning("eamuse", "{} can not be opened!", path);
return false; return false;
} }
@@ -142,7 +159,7 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
// check size // check size
if (length < 16) { if (length < 16) {
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path.string()); log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
return false; return false;
} }
@@ -160,14 +177,14 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
if (!digit && !character_big && !character_small) { if (!digit && !character_big && !character_small) {
log_warning("eamuse", log_warning("eamuse",
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)", "{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
path.string(), n); path, n);
return false; return false;
} }
} }
// info // info
log_info("eamuse", "[P{}] Inserted {}: {}", index+1, path.string(), buffer); log_info("eamuse", "[P{}] Inserted {}: {}", index+1, path, buffer);
// convert hex to bytes // convert hex to bytes
hex2bin(buffer, card); hex2bin(buffer, card);
@@ -286,6 +303,7 @@ void eamuse_coin_start_thread() {
COIN_INPUT_THREAD = new std::thread([]() { COIN_INPUT_THREAD = new std::thread([]() {
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
static bool COIN_INPUT_KEY_STATE = false; static bool COIN_INPUT_KEY_STATE = false;
timeutils::PreciseSleepTimer timer;
while (COIN_INPUT_THREAD_ACTIVE) { while (COIN_INPUT_THREAD_ACTIVE) {
// check input key // check input key
@@ -305,7 +323,7 @@ void eamuse_coin_start_thread() {
} }
// once every two frames // once every two frames
Sleep(1000 / 30); timer.sleep(1000 / 30);
} }
}); });
} }
@@ -322,6 +340,25 @@ void eamuse_pin_macro_start_thread() {
// set active // set active
PIN_MACRO_THREAD_ACTIVE = true; PIN_MACRO_THREAD_ACTIVE = true;
// a unit is eligible for auto-trigger only if all the static prerequisites are
// satisfied at startup; precomputing this lets the log hook skip per-line checks
// on values that never change at runtime
for (int unit = 0; unit < 2; unit++) {
AUTO_PIN_MACRO_PLAYER_ACTIVE[unit] =
!AUTO_PIN_MACRO_TRIGGER[unit].empty() &&
!PIN_MACRO_VALUES[unit].empty();
if(!AUTO_PIN_MACRO_TRIGGER[unit].empty() && PIN_MACRO_VALUES[unit].empty()) {
log_warning("eamuse", "Configuration error; Pin Macro empty for P{}.", unit+1);
}
}
// register scene log hook so the macro fires on the per-game trigger string,
// but only if at least one player is eligible
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
logger::hook_add(pin_macro_log_hook, nullptr);
log_info("eamuse", "AUTO_PIN_MACRO enabled");
}
// create thread // create thread
PIN_MACRO_THREAD = new std::thread([]() { PIN_MACRO_THREAD = new std::thread([]() {
uint16_t keypad_overrides[] = { uint16_t keypad_overrides[] = {
@@ -340,17 +377,23 @@ void eamuse_pin_macro_start_thread() {
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()}; size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
std::optional<uint8_t> active_unit = std::nullopt; std::optional<uint8_t> active_unit = std::nullopt;
timeutils::PreciseSleepTimer timer;
while (PIN_MACRO_THREAD_ACTIVE) { while (PIN_MACRO_THREAD_ACTIVE) {
// wait for key press // wait for key press or auto-trigger
if (!active_unit.has_value()) { if (!active_unit.has_value()) {
for (int unit = 0; unit < 2; unit++) { for (int unit = 0; unit < 2; unit++) {
if (PIN_MACRO_VALUES[unit].empty()) { if (PIN_MACRO_VALUES[unit].empty()) {
continue; continue;
} }
if (overlay_buttons && bool key_press = overlay_buttons &&
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]))) { GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]));
bool auto_request = AUTO_PIN_MACRO_REQUEST[unit].exchange(false);
if (auto_request) {
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
}
if (key_press || auto_request) {
active_unit = unit; active_unit = unit;
// Reset key index // Reset key index
pin_index[unit] = 0; pin_index[unit] = 0;
@@ -359,7 +402,7 @@ void eamuse_pin_macro_start_thread() {
} }
if (!active_unit.has_value()) { if (!active_unit.has_value()) {
Sleep(20); timer.sleep(20);
continue; continue;
} }
} }
@@ -375,22 +418,22 @@ void eamuse_pin_macro_start_thread() {
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]); eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
} }
pin_index[unit]++; pin_index[unit]++;
Sleep(100); timer.sleep(100);
// clear // clear
eamuse_set_keypad_overrides(unit, 0); eamuse_set_keypad_overrides(unit, 0);
Sleep(50); timer.sleep(50);
// end of PIN // end of PIN
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) { if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
active_unit = std::nullopt; active_unit = std::nullopt;
Sleep(120); timer.sleep(120);
} }
continue; continue;
} }
Sleep(200); timer.sleep(200);
} }
}); });
} }
@@ -402,6 +445,9 @@ void eamuse_pin_macro_stop_thread() {
delete PIN_MACRO_THREAD; delete PIN_MACRO_THREAD;
PIN_MACRO_THREAD = nullptr; PIN_MACRO_THREAD = nullptr;
} }
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
logger::hook_remove(pin_macro_log_hook, nullptr);
}
} }
void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) { void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) {
@@ -454,7 +500,7 @@ uint16_t eamuse_get_keypad_state(size_t unit) {
if (unit >= std::size(KEYPAD_STATE)) { if (unit >= std::size(KEYPAD_STATE)) {
return 0; return 0;
} }
// reset // reset
KEYPAD_STATE[unit] = KEYPAD_STATE_OVERRIDES[unit]; KEYPAD_STATE[unit] = KEYPAD_STATE_OVERRIDES[unit];
KEYPAD_STATE[unit] |= KEYPAD_STATE_OVERRIDES_BT5[unit]; KEYPAD_STATE[unit] |= KEYPAD_STATE_OVERRIDES_BT5[unit];
+1
View File
@@ -38,6 +38,7 @@ extern float AUTO_INSERT_CARD_COOLDOWN;
extern bool PIN_MACRO_ENABLED; extern bool PIN_MACRO_ENABLED;
extern std::string PIN_MACRO_VALUES[2]; extern std::string PIN_MACRO_VALUES[2];
extern std::string AUTO_PIN_MACRO_TRIGGER[2];
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card); bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id); bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
+17 -3
View File
@@ -38,6 +38,9 @@ namespace wintouchemu {
bool INJECT_MOUSE_AS_WM_TOUCH = false; bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = 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;
@@ -248,7 +251,10 @@ namespace wintouchemu {
touch_input->y -= SPICETOUCH_TOUCH_Y; touch_input->y -= SPICETOUCH_TOUCH_Y;
} }
// log_misc("wintouchemu", "mouse state ({}, {})", to_string(touch_input->x), to_string(touch_input->y)); // log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true; auto valid = true;
if (overlay::OVERLAY) { if (overlay::OVERLAY) {
@@ -410,7 +416,7 @@ namespace wintouchemu {
// same as iidx case above // same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOW) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else { } else {
@@ -439,6 +445,8 @@ namespace wintouchemu {
} }
} }
const auto now = get_performance_milliseconds();
// update touch events // update touch events
if (hWnd != nullptr) { if (hWnd != nullptr) {
@@ -458,6 +466,7 @@ 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);
@@ -484,7 +493,12 @@ namespace wintouchemu {
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to // 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 // send empty events when the mouse button is not clicked/released
if (hWnd != nullptr && USE_MOUSE) { if (hWnd != nullptr && USE_MOUSE) {
bool button_pressed = ((GetKeyState(VK_LBUTTON) & 0x100) != 0); 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 // figure out what kind of touch event to simulate
if (button_pressed && !mouse_state.last_button_pressed) { if (button_pressed && !mouse_state.last_button_pressed) {
+40 -29
View File
@@ -351,7 +351,8 @@ void ImGui_ImplSpice_NewFrame() {
// only process new input from devices if window is in focus // only process new input from devices if window is in focus
// (when not in focus, all mouse/keyboard events are treated as released) // (when not in focus, all mouse/keyboard events are treated as released)
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE; const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible;
// remember old state // remember old state
std::array<BYTE, VKEY_MAX> KeysDownOld; std::array<BYTE, VKEY_MAX> KeysDownOld;
@@ -366,14 +367,16 @@ void ImGui_ImplSpice_NewFrame() {
g_KeysDown.fill(false); g_KeysDown.fill(false);
// apply windows mouse buttons // apply windows mouse buttons
g_MouseDown[ImGuiMouseButton_Left] |= (get_async_primary_mouse()) != 0; if (accept_new_input) {
g_MouseDown[ImGuiMouseButton_Right] |= (get_async_secondary_mouse()) != 0; g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON)) != 0; g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
}
// read new keys state // read new keys state
static long mouse_wheel_last = 0; static long mouse_wheel_last = 0;
long mouse_wheel = 0; long mouse_wheel = 0;
if (RI_MGR != nullptr && accept_new_input) { if (RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get(); auto devices = RI_MGR->devices_get();
for (auto &device : devices) { for (auto &device : devices) {
switch (device.type) { switch (device.type) {
@@ -381,40 +384,46 @@ void ImGui_ImplSpice_NewFrame() {
auto &mouse = device.mouseInfo; auto &mouse = device.mouseInfo;
// mouse button triggers // mouse button triggers
if (GetSystemMetrics(SM_SWAPBUTTON)) { if (accept_new_input) {
if (mouse->key_states[rawinput::MOUSEBTN_RIGHT]) { if (GetSystemMetrics(SM_SWAPBUTTON)) {
g_MouseDown[ImGuiMouseButton_Left] = true; if (mouse->key_states[rawinput::MOUSEBTN_RIGHT]) {
g_MouseDown[ImGuiMouseButton_Left] = true;
}
if (mouse->key_states[rawinput::MOUSEBTN_LEFT]) {
g_MouseDown[ImGuiMouseButton_Right] = true;
}
} else {
if (mouse->key_states[rawinput::MOUSEBTN_LEFT]) {
g_MouseDown[ImGuiMouseButton_Left] = true;
}
if (mouse->key_states[rawinput::MOUSEBTN_RIGHT]) {
g_MouseDown[ImGuiMouseButton_Right] = true;
}
} }
if (mouse->key_states[rawinput::MOUSEBTN_LEFT]) { if (mouse->key_states[rawinput::MOUSEBTN_MIDDLE]) {
g_MouseDown[ImGuiMouseButton_Right] = true; g_MouseDown[ImGuiMouseButton_Middle] = true;
} }
} else {
if (mouse->key_states[rawinput::MOUSEBTN_LEFT]) {
g_MouseDown[ImGuiMouseButton_Left] = true;
}
if (mouse->key_states[rawinput::MOUSEBTN_RIGHT]) {
g_MouseDown[ImGuiMouseButton_Right] = true;
}
}
if (mouse->key_states[rawinput::MOUSEBTN_MIDDLE]) {
g_MouseDown[ImGuiMouseButton_Middle] = true;
} }
// final mouse wheel value should be all devices combined // final mouse wheel value should be all devices combined
// need to continuously calculate mouse wheel position even when not in focus
// to avoid values jumping (since mouse wheel values are absolute not relative)
mouse_wheel += mouse->pos_wheel; mouse_wheel += mouse->pos_wheel;
break; break;
} }
case rawinput::KEYBOARD: { case rawinput::KEYBOARD: {
if (accept_new_input) {
// iterate all virtual key codes // iterate all virtual key codes
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) { for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
// get state (combined from all pages) // get state (combined from all pages)
auto &key_states = device.keyboardInfo->key_states; auto &key_states = device.keyboardInfo->key_states;
for (size_t page_index = 0; page_index < 1024; page_index += 256) { for (size_t page_index = 0; page_index < 1024; page_index += 256) {
g_KeysDown[vKey] |= key_states[page_index + vKey]; g_KeysDown[vKey] |= key_states[page_index + vKey];
}
} }
} }
break; break;
} }
default: default:
@@ -464,9 +473,11 @@ void ImGui_ImplSpice_NewFrame() {
} }
// set mouse wheel // set mouse wheel
auto wheel_diff = mouse_wheel - mouse_wheel_last; long wheel_diff = mouse_wheel - mouse_wheel_last;
mouse_wheel_last = mouse_wheel; mouse_wheel_last = mouse_wheel;
io.AddMouseWheelEvent(0, wheel_diff); if (wheel_diff != 0 && accept_new_input) {
io.AddMouseWheelEvent(0, wheel_diff);
}
// update OS mouse position // update OS mouse position
const auto old_mouse_pos = io.MousePos; const auto old_mouse_pos = io.MousePos;
+3 -3
View File
@@ -415,7 +415,7 @@ void overlay::SpiceOverlay::init() {
window_sub = new overlay::windows::DRSDanceFloorDisplay(this); window_sub = new overlay::windows::DRSDanceFloorDisplay(this);
} else if (avs::game::is_model("KFC")) { } else if (avs::game::is_model("KFC")) {
window_sub = new overlay::windows::SDVXSubScreen(this); window_sub = new overlay::windows::SDVXSubScreen(this);
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model()) {
window_sub = new overlay::windows::GitaDoraSubScreen(this); window_sub = new overlay::windows::GitaDoraSubScreen(this);
} else if (games::popn::is_pikapika_model()) { } else if (games::popn::is_pikapika_model()) {
window_sub = new overlay::windows::PopnSubScreen(this); window_sub = new overlay::windows::PopnSubScreen(this);
@@ -733,13 +733,13 @@ void overlay::SpiceOverlay::add_font(const char* font, ImFontConfig* config, con
full_path += font; full_path += font;
if (fileutils::file_exists(full_path)) { if (fileutils::file_exists(full_path)) {
log_misc("overlay", "loading font: {}", full_path.string()); log_misc("overlay", "loading font: {}", full_path);
ImGui::GetIO().Fonts->AddFontFromFileTTF( ImGui::GetIO().Fonts->AddFontFromFileTTF(
full_path.string().c_str(), full_path.string().c_str(),
13.0f, 13.0f,
config, config,
glyphs); glyphs);
} else { } else {
log_misc("overlay", "font not found: {}", full_path.string()); log_misc("overlay", "font not found: {}", full_path);
} }
} }
+249 -151
View File
@@ -948,7 +948,7 @@ namespace overlay::windows {
// grab current keyboard state // grab current keyboard state
for (unsigned short int i = 0x01; i < 0xFF; i++) { for (unsigned short int i = 0x01; i < 0xFF; i++) {
buttons_keyboard_state[i] = GetAsyncKeyState(i) != 0; buttons_keyboard_state[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
} }
} }
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) { if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
@@ -1063,6 +1063,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setInvert(false); button->setInvert(false);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED); button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
@@ -1205,6 +1206,16 @@ namespace overlay::windows {
case rawinput::HID: { case rawinput::HID: {
auto hid = device->hidInfo; auto hid = device->hidInfo;
struct temp_button {
std::string device_name;
unsigned short vkey;
ButtonAnalogType analog_type;
};
// use this to prioritize button binding over value types
// (for controllers that output both)
std::optional<temp_button> button_to_bind;
// ignore touchscreen and digitizer button inputs // ignore touchscreen and digitizer button inputs
// digitizer has funky stuff like "Touch Valid" "Data Valid" always held high // digitizer has funky stuff like "Touch Valid" "Data Valid" always held high
if (!rawinput::touch::is_touchscreen(device) && if (!rawinput::touch::is_touchscreen(device) &&
@@ -1218,18 +1229,11 @@ namespace overlay::windows {
// check if button is down // check if button is down
if (button_states[i]) { if (button_states[i]) {
button_to_bind = {
// bind key device->name,
button->setDeviceIdentifier(device->name); static_cast<unsigned short>(button_index + i),
button->setVKey(static_cast<unsigned short>(button_index + i)); BAT_NONE
button->setAnalogType(BAT_NONE); };
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break; break;
} }
} }
@@ -1238,93 +1242,100 @@ namespace overlay::windows {
} }
// value caps // value caps
auto value_states = &hid->value_states; if (!button_to_bind.has_value()) {
auto bind_value_states = &hid->bind_value_states; auto value_states = &hid->value_states;
auto value_names = &hid->value_caps_names; auto bind_value_states = &hid->bind_value_states;
for (size_t i = 0; i < value_states->size(); i++) { auto value_names = &hid->value_caps_names;
auto &state = value_states->at(i); for (size_t i = 0; i < value_states->size(); i++) {
auto &bind_state = bind_value_states->at(i); auto &state = value_states->at(i);
auto &value_name = value_names->at(i); auto &bind_state = bind_value_states->at(i);
auto &value_name = value_names->at(i);
// check for valid axis names // check for valid axis names
if (value_name == "X" || if (value_name == "X" ||
value_name == "Y" || value_name == "Y" ||
value_name == "Rx" || value_name == "Rx" ||
value_name == "Ry" || value_name == "Ry" ||
value_name == "Z") value_name == "Z")
{ {
// check if axis is in activation area // check if axis is in activation area
float normalized = (state - 0.5f) * 2.f; float normalized = (state - 0.5f) * 2.f;
float diff = std::fabs(state - bind_state); float diff = std::fabs(state - bind_state);
if (std::fabs(normalized) > 0.9f && diff > 0.1f) { if (std::fabs(normalized) > 0.9f && diff > 0.1f) {
auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE; auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE;
// bind value // bind value
button->setDeviceIdentifier(device->name); button_to_bind = {
button->setVKey(static_cast<unsigned short>(i)); device->name,
button->setAnalogType(bat); static_cast<unsigned short>(i),
button->setDebounceUp(0.0); bat
button->setDebounceDown(0.0); };
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
// usually, turntables are X, knobs are X and Y // usually, turntables are X, knobs are X and Y
// gamepad triggers are Z, and Rx/Ry are right thumb stick // gamepad triggers are Z, and Rx/Ry are right thumb stick
// one day we will label all I/O modules and flag which one are button-as-analog // one day we will label all I/O modules and flag which one are button-as-analog
// so that we don't have to do string comparions like below // so that we don't have to do string comparions like below
if ((value_name == "X" || value_name == "Y") && if ((value_name == "X" || value_name == "Y") &&
(button->getName().find("Press") == std::string::npos) && // museca (button->getName().find("Press") == std::string::npos) && // museca
(button->getName().find("Slowdown") == std::string::npos) && // bishibashi (button->getName().find("Slowdown") == std::string::npos) && // bishibashi
(button->getName().find("TT+") != std::string::npos || (button->getName().find("TT+") != std::string::npos ||
button->getName().find("TT-") != std::string::npos || // iidx button->getName().find("TT-") != std::string::npos || // iidx
button->getName().find("Knob") != std::string::npos || // gitadora guitar button->getName().find("Knob") != std::string::npos || // gitadora guitar
button->getName().find("Disk") != std::string::npos || // museca, bishibashi button->getName().find("Disk") != std::string::npos || // museca, bishibashi
button->getName().find("VOL-") != std::string::npos)) { // sdvx button->getName().find("P1 Panel") != std::string::npos || // ddr
this->analog_as_button_warning_show_next_frame = button->getName().find("P2 Panel") != std::string::npos || // ddr
std::make_pair(button->getName(), alt_index); button->getName().find("VOL-") != std::string::npos)) { // sdvx
this->analog_as_button_warning_show_next_frame =
std::make_pair(button->getName(), alt_index);
}
break;
} else if (diff > 0.3f) {
bind_state = state;
} }
break;
} else if (diff > 0.3f) {
bind_state = state;
} }
}
// hat switch // hat switch
if (value_name == "Hat switch") { if (value_name == "Hat switch") {
// get hat switch values // get hat switch values
ButtonAnalogType buffer[3], buffer_bind[3]; ButtonAnalogType buffer[3], buffer_bind[3];
Button::getHatSwitchValues(state, buffer); Button::getHatSwitchValues(state, buffer);
Button::getHatSwitchValues(bind_state, buffer_bind); Button::getHatSwitchValues(bind_state, buffer_bind);
// check the first entry only // check the first entry only
if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) { if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) {
// bind value // bind value
button->setDeviceIdentifier(device->name); button_to_bind = {
button->setVKey(static_cast<unsigned short>(i)); device->name,
button->setAnalogType(buffer[0]); static_cast<unsigned short>(i),
button->setDebounceUp(0.0); buffer[0]
button->setDebounceDown(0.0); };
button->setVelocityThreshold(0); break;
::Config::getInstance().updateBinding( }
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
} }
} }
} }
if (button_to_bind.has_value()) {
const auto &b = button_to_bind.value();
button->setDeviceIdentifier(b.device_name);
button->setVKey(b.vkey);
button->setAnalogType(b.analog_type);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
}
break; break;
} }
case rawinput::MIDI: { case rawinput::MIDI: {
@@ -1360,6 +1371,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(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()]);
@@ -1385,6 +1397,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_MIDI_CTRL_PRECISION); button->setAnalogType(BAT_MIDI_CTRL_PRECISION);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1411,6 +1424,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_MIDI_CTRL_SINGLE); button->setAnalogType(BAT_MIDI_CTRL_SINGLE);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1437,6 +1451,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_MIDI_CTRL_ONOFF); button->setAnalogType(BAT_MIDI_CTRL_ONOFF);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1462,6 +1477,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_MIDI_PITCH_DOWN); button->setAnalogType(BAT_MIDI_PITCH_DOWN);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1482,6 +1498,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_MIDI_PITCH_UP); button->setAnalogType(BAT_MIDI_PITCH_UP);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1511,6 +1528,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1597,7 +1615,7 @@ namespace overlay::windows {
// use care when iterating over the result (could result in torn reads) // use care when iterating over the result (could result in torn reads)
bool keyboard_state_new[sizeof(buttons_keyboard_state)]; bool keyboard_state_new[sizeof(buttons_keyboard_state)];
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) { for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
keyboard_state_new[i] = GetAsyncKeyState(i) != 0; keyboard_state_new[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
} }
// detect key presses // detect key presses
@@ -1635,6 +1653,7 @@ namespace overlay::windows {
button->setVKey(vKey); button->setVKey(vKey);
button->setDebounceUp(0.0); button->setDebounceUp(0.0);
button->setDebounceDown(0.0); button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
@@ -1686,6 +1705,9 @@ namespace overlay::windows {
button->setDeviceIdentifier(device.name); button->setDeviceIdentifier(device.name);
// reset controls when switching devices // reset controls when switching devices
button->setAnalogType(ButtonAnalogType::BAT_NONE); button->setAnalogType(ButtonAnalogType::BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
button->setVKey(0); button->setVKey(0);
button->setInvert(false); button->setInvert(false);
@@ -1981,6 +2003,7 @@ namespace overlay::windows {
int vKey = button->getVKey(); int vKey = button->getVKey();
if (ImGui::InputInt(button->isNaive() ? "Virtual Key" : "Index", &vKey, 1, 1)) { if (ImGui::InputInt(button->isNaive() ? "Virtual Key" : "Index", &vKey, 1, 1)) {
button->setVKey(vKey); button->setVKey(vKey);
button->setInvert(false);
} }
if (ImGui::IsItemDeactivatedAfterEdit()) { if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true; dirty = true;
@@ -2048,11 +2071,64 @@ namespace overlay::windows {
"This setting will add noticable input lag."); "This setting will add noticable input lag.");
} }
// invert // invert (widget hidden for naive + 0xff)
bool invert = button->getInvert(); if (!(button->isNaive() && button->getVKey() == INVALID_VKEY)) {
if (ImGui::Checkbox("Invert", &invert)) { bool invert = button->getInvert();
button->setInvert(invert); if (ImGui::Checkbox("Invert Resulting Value", &invert)) {
dirty = true; button->setInvert(invert);
dirty = true;
}
}
// always on (naive + vkey 0xff + invert)
if (button->isNaive() && alt_index == 0) {
bool always_on = button->isNaive() && button->getVKey() == INVALID_VKEY && button->getInvert();
if (ImGui::Checkbox("Always On", &always_on)) {
if (always_on) {
button->setVKey(INVALID_VKEY);
button->setInvert(true);
} else {
button->setInvert(false);
}
dirty = true;
}
}
// bat threshold
if (device != nullptr && device->type == rawinput::HID &&
(button->getAnalogType() == BAT_POSITIVE || button->getAnalogType() == BAT_NEGATIVE)) {
int bat_threshold = button->getBatThreshold();
bool is_active = (bat_threshold > 0);
if (ImGui::Checkbox("Custom Analog Threshold", &is_active)) {
if (!is_active) {
button->setBatThreshold(0);
} else if (bat_threshold == 0) {
button->setBatThreshold(60);
}
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker(
"Assuming that 50% is the center, value exceeding 60% triggers the button by default. "
"With this option, you can customize the trigger threshold.\n\n"
"Setting this to 51% means tiny movements will trigger the button (small deadzone), "
"while setting this to 99% means the button will only trigger at maximum value (large deadzone).\n\n"
"Setting this to below 50% adjusts the neutral / center position.");
if (is_active) {
if (ImGui::SliderInt(
"Analog Threshold",
&bat_threshold,
1, 99,
"%d%%",
ImGuiSliderFlags_AlwaysClamp)) {
button->setBatThreshold(bat_threshold);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
}
} }
// state display // state display
@@ -2133,9 +2209,9 @@ namespace overlay::windows {
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginTable("AnalogsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) { if (ImGui::BeginTable("AnalogsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(220)); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(240));
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(100)); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(80));
// check if empty // check if empty
if (!analogs || analogs->empty()) { if (!analogs || analogs->empty()) {
@@ -2194,8 +2270,9 @@ namespace overlay::windows {
} }
// analog binding // analog binding
const std::string title = "Analog Binding (" + analog.getName() + ")";
if (ImGui::Button("Set")) { if (ImGui::Button("Set")) {
ImGui::OpenPopup("Analog Binding"); ImGui::OpenPopup(title.c_str());
// get devices // get devices
this->analogs_devices.clear(); this->analogs_devices.clear();
@@ -2225,7 +2302,7 @@ namespace overlay::windows {
} }
} }
edit_analog_popup(analog); edit_analog_popup(analog, title);
// row hover detection (invisible selectable that spans entire row) // row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable(); ImGui::InvisibleTableRowSelectable();
@@ -2239,8 +2316,8 @@ namespace overlay::windows {
} }
} }
void Config::edit_analog_popup(Analog &analog) { void Config::edit_analog_popup(Analog &analog, std::string title) {
if (ImGui::BeginPopupModal("Analog Binding", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::BeginPopupModal(title.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
// device selector // device selector
auto analog_device_changed = ImGui::Combo( auto analog_device_changed = ImGui::Combo(
@@ -2447,31 +2524,45 @@ namespace overlay::windows {
const bool value_changed = const bool value_changed =
ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f"); ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f");
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker( if (device->type == rawinput::HID && analog.getType() != GameAPI::Analogs::AnalogType::Circular) {
"Adjust floating point multiplier to relative movement.\n\n" ImGui::HelpMarker(
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).\n\n" "Adjust the analog sensitivity curve; "
"Dependent on how often the game polls for input. Intended for angular input (knobs, turntables)"); "values <1.0 are less sensitive around neutral, "
"values >1.0 are more sensitive.");
} else {
ImGui::HelpMarker(
"Adjust floating point multiplier to relative movement.\n\n"
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).");
}
if (value_changed) { if (value_changed) {
analog.setSensitivity(sensitivity * sensitivity); analog.setSensitivity(sensitivity * sensitivity);
} }
} }
if (device->type == rawinput::HID || device->type == rawinput::MIDI) {
// hide deadzone for circular analog since it doesn't make any sense (unless in relative mode)
if ((device->type == rawinput::HID || device->type == rawinput::MIDI) &&
((analog.getType() != GameAPI::Analogs::AnalogType::Circular) || analog.isRelativeMode())) {
auto deadzone = analog.getDeadzone(); auto deadzone = analog.getDeadzone();
// for back compat (before each analog had a type)
if (deadzone < 0.f) {
deadzone = -deadzone;
analog.setDeadzone(deadzone);
}
const bool value_changed = const bool value_changed =
ImGui::SliderFloat("Deadzone", &deadzone, -0.999f, 0.999f, "%.3f"); ImGui::SliderFloat("Deadzone", &deadzone, 0.f, 0.999f, "%.3f");
if (value_changed) { if (value_changed) {
analog.setDeadzone(deadzone); analog.setDeadzone(deadzone);
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker("Positive values specify a deadzone around the middle.\n" ImGui::HelpMarker("Specify the deadzone that gets applied to at-rest (neutral) value.");
"Negative values specify a deadzone from the minimum value.");
// deadzone mirror // deadzone mirror
bool deadzone_mirror = analog.getDeadzoneMirror(); bool deadzone_mirror = analog.getDeadzoneMirror();
ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror); ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror);
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker("Positive deadzone values cut off at edges instead.\n" ImGui::HelpMarker("Apply deadzone to extreme value(s) instead of neutral.");
"Negative deadzone values cut off at maximum value instead.");
if (deadzone_mirror != analog.getDeadzoneMirror()) { if (deadzone_mirror != analog.getDeadzoneMirror()) {
analog.setDeadzoneMirror(deadzone_mirror); analog.setDeadzoneMirror(deadzone_mirror);
} }
@@ -2490,47 +2581,50 @@ namespace overlay::windows {
if (this->analogs_devices_selected >= 0) { if (this->analogs_devices_selected >= 0) {
const auto device = this->analogs_devices.at(this->analogs_devices_selected); const auto device = this->analogs_devices.at(this->analogs_devices_selected);
if (device->type == rawinput::HID) { if (device->type == rawinput::HID) {
// smoothing
bool smoothing = analog.getSmoothing(); if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
ImGui::BeginDisabled(analog.isRelativeMode()); // smoothing
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing); bool smoothing = analog.getSmoothing();
ImGui::SameLine(); ImGui::BeginDisabled(analog.isRelativeMode());
ImGui::HelpMarker( ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
"Apply a moving average algorithm; intended for angular input (knobs, turntables). " ImGui::SameLine();
"Adds a slight bit of latency to input as the algorithm averages out recent input. " ImGui::HelpMarker(
"Only use in dire situations where the input is too jittery for the game."); "Apply a moving average algorithm; intended for angular input (knobs, turntables). "
ImGui::EndDisabled(); "Adds a slight bit of latency to input as the algorithm averages out recent input. "
if (smoothing != analog.getSmoothing()) { "Only use in dire situations where the input is too jittery for the game.");
analog.setSmoothing(smoothing); ImGui::EndDisabled();
if (smoothing != analog.getSmoothing()) {
analog.setSmoothing(smoothing);
}
// relative input mode
bool relative_analog = analog.isRelativeMode();
ImGui::Checkbox("Relative Axis", &relative_analog);
ImGui::SameLine();
ImGui::HelpMarker(
"Use relative directional input instead of positional values. "
"Can be used to translate analog sticks to knob input.\n\n"
"At default settings, max speed is one revolution per second.\n\n"
"Adjust sensitivity to increase/decrease max speed, and deadzone to prevent jitter.");
if (relative_analog != analog.isRelativeMode()) {
analog.setRelativeMode(relative_analog);
}
} }
// relative input mode // delay
bool relative_analog = analog.isRelativeMode(); int delay = analog.getDelayMs();
ImGui::Checkbox("Relative Axis (experimental)", &relative_analog); if (ImGui::InputInt("Delay (ms)", &delay, 1, 1)) {
ImGui::SameLine(); delay = std::clamp(delay, 0, 250);
ImGui::HelpMarker( analog.setDelayMs(delay);
"Use relative directional input instead of positional values.\n\n"
"Can be used to translate analog sticks to knob input, for example.\n\n"
"WARNING: speed depends on how often the game polls for input! "
"Strongly recommended that you go into the game's test menu instead "
"of adjusting in spicecfg.");
if (relative_analog != analog.isRelativeMode()) {
analog.setRelativeMode(relative_analog);
}
// delay buffer
int delay = analog.getDelayBufferDepth();
ImGui::InputInt("Delay (experimental)", &delay, 1, 10);
if (ImGui::IsItemDeactivatedAfterEdit()) {
delay = CLAMP(delay, 0, 256);
analog.setDelayBufferDepth(delay);
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker( ImGui::HelpMarker(
"Adds a delay to input. This is poll-based, not time-based.\n\n" "Adds an artificial delay, in milliseconds.\n\n"
"WARNING: delay depends on how often the game polls for input! " "Value read by the game engine will be delayed AT MOST this amount of time (could be slightly less).\n\n"
"Strongly recommended that you go into the game's test menu instead " "To minimize quantization error, delay value should be a multiple of the game's poll rate, "
"of adjusting in spicecfg."); "add 0.5ms of buffer, and then round up to the nearest millisecond.\n\n"
"If you aren't sure, assume the game polls for input at the same rate as the display FPS."
);
} }
} }
@@ -2541,10 +2635,12 @@ namespace overlay::windows {
ImGui::ProgressBar(value); ImGui::ProgressBar(value);
// centered knob preview // centered knob preview
const float knob_size = 64.f; if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
auto width = ImGui::GetContentRegionAvail().x - knob_size; const float knob_size = 64.f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2)); auto width = ImGui::GetContentRegionAvail().x - knob_size;
ImGui::Knob(value, knob_size); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2));
ImGui::Knob(value, knob_size);
}
// update analog // update analog
if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) { if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) {
@@ -5283,6 +5379,7 @@ namespace overlay::windows {
btn.setInvert(entry->invert); btn.setInvert(entry->invert);
btn.setDebounceUp(entry->debounce_up); btn.setDebounceUp(entry->debounce_up);
btn.setDebounceDown(entry->debounce_down); btn.setDebounceDown(entry->debounce_down);
btn.setBatThreshold(entry->bat_threshold);
btn.setVelocityThreshold(entry->velocity_threshold); btn.setVelocityThreshold(entry->velocity_threshold);
::Config::getInstance().updateBinding(game, btn, -1); ::Config::getInstance().updateBinding(game, btn, -1);
} else { } else {
@@ -5293,6 +5390,7 @@ namespace overlay::windows {
alt_btn.setInvert(entry->invert); alt_btn.setInvert(entry->invert);
alt_btn.setDebounceUp(entry->debounce_up); alt_btn.setDebounceUp(entry->debounce_up);
alt_btn.setDebounceDown(entry->debounce_down); alt_btn.setDebounceDown(entry->debounce_down);
alt_btn.setBatThreshold(entry->bat_threshold);
alt_btn.setVelocityThreshold(entry->velocity_threshold); alt_btn.setVelocityThreshold(entry->velocity_threshold);
alt_btn.setTemporary(true); alt_btn.setTemporary(true);
btn.getAlternatives().push_back(alt_btn); btn.getAlternatives().push_back(alt_btn);
@@ -5339,7 +5437,7 @@ namespace overlay::windows {
a.setSmoothing(ta.smoothing); a.setSmoothing(ta.smoothing);
a.setMultiplier(ta.multiplier); a.setMultiplier(ta.multiplier);
a.setRelativeMode(ta.relative_mode); a.setRelativeMode(ta.relative_mode);
a.setDelayBufferDepth(ta.delay_buffer_depth); a.setDelayMs(ta.delay_ms);
::Config::getInstance().updateBinding(game, a); ::Config::getInstance().updateBinding(game, a);
break; break;
} }
+1 -1
View File
@@ -162,7 +162,7 @@ namespace overlay::windows {
unsigned int get_keypad_top_row(const Button &button); unsigned int get_keypad_top_row(const Button &button);
void build_analogs(const std::string &name, std::vector<Analog> *analogs); void build_analogs(const std::string &name, std::vector<Analog> *analogs);
void edit_analog_popup(Analog &analog); void edit_analog_popup(Analog &analog, std::string title);
void update() override; void update() override;
void stop_lights_test(); void stop_lights_test();
@@ -17,6 +17,7 @@ namespace overlay::windows {
bool invert = false; bool invert = false;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0;
unsigned short velocity_threshold = 0; unsigned short velocity_threshold = 0;
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; } bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
@@ -31,6 +32,7 @@ namespace overlay::windows {
e.invert = btn.getInvert(); e.invert = btn.getInvert();
e.debounce_up = btn.getDebounceUp(); e.debounce_up = btn.getDebounceUp();
e.debounce_down = btn.getDebounceDown(); e.debounce_down = btn.getDebounceDown();
e.bat_threshold = btn.getBatThreshold();
e.velocity_threshold = btn.getVelocityThreshold(); e.velocity_threshold = btn.getVelocityThreshold();
return e; return e;
} }
@@ -70,7 +72,7 @@ namespace overlay::windows {
bool smoothing = false; bool smoothing = false;
int multiplier = 1; int multiplier = 1;
bool relative_mode = false; bool relative_mode = false;
int delay_buffer_depth = 0; uint32_t delay_ms = 0;
bool is_device() const { return !device_identifier.empty(); } bool is_device() const { return !device_identifier.empty(); }
bool is_unbound() const { return device_identifier.empty() && index == 0xFF; } bool is_unbound() const { return device_identifier.empty() && index == 0xFF; }
@@ -88,7 +90,7 @@ namespace overlay::windows {
smoothing = a.getSmoothing(); smoothing = a.getSmoothing();
multiplier = a.getMultiplier(); multiplier = a.getMultiplier();
relative_mode = a.isRelativeMode(); relative_mode = a.isRelativeMode();
delay_buffer_depth = a.getDelayBufferDepth(); delay_ms = a.getDelayMs();
} }
}; };
+1 -1
View File
@@ -71,7 +71,7 @@ namespace overlay::windows {
sub = "Show Valkyrie Subscreen"; sub = "Show Valkyrie Subscreen";
} else if (avs::game::is_model("REC")) { } else if (avs::game::is_model("REC")) {
sub = "Show DRS Dance Floor"; sub = "Show DRS Dance Floor";
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model()) {
sub = "Show GITADORA Subscreen"; sub = "Show GITADORA Subscreen";
} else if (games::popn::is_pikapika_model()) { } else if (games::popn::is_pikapika_model()) {
sub = "Show Pop'n Subscreen"; sub = "Show Pop'n Subscreen";
+6
View File
@@ -11,6 +11,12 @@ namespace overlay::windows {
if (!games::gitadora::is_arena_model()) { if (!games::gitadora::is_arena_model()) {
this->disabled_message = "Requires Arena Model!"; this->disabled_message = "Requires Arena Model!";
} else if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) {
if (GRAPHICS_WINDOWED) {
this->disabled_message = "Use the dedicated subscreen window!";
} else {
this->disabled_message = "Subscreen overlay disabled by user; enable single window mode for overlay.";
}
} }
this->resize_callback = keep_10_by_16; this->resize_callback = keep_10_by_16;
+25
View File
@@ -3,6 +3,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/popn/popn.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "overlay/imgui/extensions.h" #include "overlay/imgui/extensions.h"
@@ -42,6 +43,8 @@ namespace overlay::windows {
void Keypad::build_content() { void Keypad::build_content() {
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) { if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
build_tdj_keypad(); build_tdj_keypad();
} else if (games::popn::is_pikapika_model()) {
build_popn_pika_keypad();
} else { } else {
build_keypad(); build_keypad();
} }
@@ -132,4 +135,26 @@ namespace overlay::windows {
eamuse_set_keypad_overrides_overlay(this->unit, 0); eamuse_set_keypad_overrides_overlay(this->unit, 0);
} }
} }
void Keypad::build_popn_pika_keypad() {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("Keypad disabled in Pop'n Pikapika model!\nUse subscreen overlay.");
ImGui::SameLine();
ImGui::WarnMarker(
nullptr,
"Pop'n Music Pikapika cabinets do not have any keypads; they use the subscreen.\n\n"
"Fullscreen mode: bind a key in Overlay tab, and press it in game to show the subscreen, "
"then use your mouse to click. Page Up button is the default binding.\n\n"
"Windowed mode: look for the second window in the taskbar (or ALT+TAB).\n\n"
"Windowed mode with -popnnosub: bring up the subscreen overlay (default Page Up).\n\n"
);
ImGui::Spacing();
if (ImGui::Button("Insert Card")) {
eamuse_set_keypad_overrides_overlay(this->unit, 1 << EAM_IO_INSERT);
} else {
eamuse_set_keypad_overrides_overlay(this->unit, 0);
}
}
} }
+1
View File
@@ -10,6 +10,7 @@ namespace overlay::windows {
size_t unit = 0; size_t unit = 0;
void build_keypad(); void build_keypad();
void build_tdj_keypad(); void build_tdj_keypad();
void build_popn_pika_keypad();
public: public:
@@ -74,6 +74,23 @@ namespace overlay::windows {
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_27", "", "", true, "Key 27", ""}, {"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_27", "", "", true, "Key 27", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_28", "", "", true, "Key 28", ""}, {"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_28", "", "", true, "Key 28", ""},
// icedragon.io snek board, ddr mode
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "neon", "", "", false, "Neon", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "mar p1 upper", "", "", false, "P1 Halogen Upper", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "mar p1 lower", "", "", false, "P1 Halogen Lower", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "mar p2 upper", "", "", false, "P2 Halogen Upper", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "mar p2 lower", "", "", false, "P2 Halogen Lower", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p1 buttons", "", "", false, "P1 Button", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p2 buttons", "", "", false, "P2 Button", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p1 up", "", "", false, "P1 Foot Up", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p1 down", "", "", false, "P1 Foot Down", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p1 left", "", "", false, "P1 Foot Left", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p1 right", "", "", false, "P1 Foot Right", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p2 up", "", "", false, "P2 Foot Up", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p2 down", "", "", false, "P2 Foot Down", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p2 left", "", "", false, "P2 Foot Left", ""},
{"snek lights", "Dance Dance Revolution", 0x2e8a, 0x10a8, "p2 right", "", "", false, "P2 Foot Right", ""},
}; };
static const int LIGHT_MATCH_MAP_COUNT = static const int LIGHT_MATCH_MAP_COUNT =
+29 -24
View File
@@ -95,6 +95,10 @@ namespace overlay::windows {
} }
// utility // utility
std::string displayPath(const std::filesystem::path &path) {
return fmt::format(FMT_STRING("{}"), path);
}
std::string getFromUrl(const std::string& dll_name, const std::string& url) { std::string getFromUrl(const std::string& dll_name, const std::string& url) {
log_info("patchmanager", "getting patches from URL: {}, for file: {}", url, dll_name); log_info("patchmanager", "getting patches from URL: {}, for file: {}", url, dll_name);
std::string result; std::string result;
@@ -226,7 +230,7 @@ namespace overlay::windows {
if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) { if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) {
this->config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value(); this->config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value();
log_info("patchmanager", "using custom config file path: {}", this->config_path.string().c_str()); log_info("patchmanager", "using custom config file path: {}", this->config_path);
} else { } else {
this->config_path = this->config_path =
fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json"); fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json");
@@ -330,7 +334,7 @@ namespace overlay::windows {
"Wrong path? Run spicecfg from the correct directory, or fix your modules parameter before launching spicecfg.\n" "Wrong path? Run spicecfg from the correct directory, or fix your modules parameter before launching spicecfg.\n"
"Make sure you're not using a different one when launching the game."); "Make sure you're not using a different one when launching the game.");
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("Modules Path: %s", MODULE_PATH.string().c_str()); ImGui::Text("Modules Path: %s", displayPath(MODULE_PATH).c_str());
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::DummyMarker(); ImGui::DummyMarker();
@@ -878,7 +882,7 @@ namespace overlay::windows {
} }
void PatchManager::hard_apply_patches() { void PatchManager::hard_apply_patches() {
std::vector<std::string> written_list; std::vector<std::filesystem::path> written_list;
for (auto& patch : patches) { for (auto& patch : patches) {
switch (patch.type) { switch (patch.type) {
case PatchType::Memory: case PatchType::Memory:
@@ -1218,7 +1222,7 @@ namespace overlay::windows {
log_info( log_info(
"patchmanager", "patchmanager",
"file: {}, patch id: {}, build timestamp of dll: {:%Y-%m-%d %H:%M}", "file: {}, patch id: {}, build timestamp of dll: {:%Y-%m-%d %H:%M}",
dll_path.has_filename() ? dll_path.filename().string() : dll_path.string(), dll_path.has_filename() ? dll_path.filename() : dll_path,
identifier, identifier,
time); time);
} }
@@ -1623,7 +1627,7 @@ namespace overlay::windows {
// save to file // save to file
std::filesystem::path save_path = LOCAL_PATCHES_PATH / (identifier + ".json"); std::filesystem::path save_path = LOCAL_PATCHES_PATH / (identifier + ".json");
fileutils::text_write(save_path, patches_json); fileutils::text_write(save_path, patches_json);
log_info("patchmanager", "remotely fetched JSON saved to: {}", save_path.string()); log_info("patchmanager", "remotely fetched JSON saved to: {}", save_path);
return true; return true;
} else { } else {
log_warning("patchmanager", "failed to fetch patches JSON for {}", dll_name); log_warning("patchmanager", "failed to fetch patches JSON for {}", dll_name);
@@ -1675,19 +1679,19 @@ namespace overlay::windows {
const size_t patches_size_previous = patches.size(); const size_t patches_size_previous = patches.size();
for (const std::filesystem::path& patches_json_path: LOCAL_PATCHES_JSON_PATHS) { for (const std::filesystem::path& patches_json_path: LOCAL_PATCHES_JSON_PATHS) {
if (!fileutils::file_exists(patches_json_path)) { if (!fileutils::file_exists(patches_json_path)) {
log_misc("patchmanager", "file does not exist, skipping: {}", patches_json_path.string()); log_misc("patchmanager", "file does not exist, skipping: {}", patches_json_path);
continue; continue;
} }
log_misc("patchmanager", "reading from patches.json: {}", patches_json_path.string()); log_misc("patchmanager", "reading from patches.json: {}", patches_json_path);
std::string content = fileutils::text_read(patches_json_path); std::string content = fileutils::text_read(patches_json_path);
append_patches(content, apply_patches, filter); append_patches(content, apply_patches, filter);
const auto new_patches = patches.size() - patches_size_previous; const auto new_patches = patches.size() - patches_size_previous;
log_info("patchmanager", "loaded {} patches from: {}", new_patches, patches_json_path.string()); log_info("patchmanager", "loaded {} patches from: {}", new_patches, patches_json_path);
if (0 < new_patches) { if (0 < new_patches) {
ret = true; ret = true;
ACTIVE_JSON_FILE = patches_json_path.string(); ACTIVE_JSON_FILE = displayPath(patches_json_path);
break; break;
} }
} }
@@ -1723,21 +1727,21 @@ namespace overlay::windows {
if (fileutils::file_exists(firstPath) || !extraDlls.empty()) { if (fileutils::file_exists(firstPath) || !extraDlls.empty()) {
if (fileutils::file_exists(firstPath)) { if (fileutils::file_exists(firstPath)) {
log_info("patchmanager", "loaded patches for {} from {}", firstDll, firstPath.string()); log_info("patchmanager", "loaded patches for {} from {}", firstDll, firstPath);
std::string content = fileutils::text_read(firstPath); std::string content = fileutils::text_read(firstPath);
append_patches(content, apply_patches, nullptr, first_id); append_patches(content, apply_patches, nullptr, first_id);
ACTIVE_JSON_FILE = firstPath.string(); ACTIVE_JSON_FILE = displayPath(firstPath);
} }
for (const std::string& dll : extraDlls) { for (const std::string& dll : extraDlls) {
auto extraId = get_game_identifier(MODULE_PATH / dll); auto extraId = get_game_identifier(MODULE_PATH / dll);
auto extraPath = std::filesystem::path(fmt::format("patches/{}.json", extraId)); auto extraPath = std::filesystem::path(fmt::format("patches/{}.json", extraId));
log_info("patchmanager", "loaded patches for {} from {}", dll, extraPath.string()); log_info("patchmanager", "loaded patches for {} from {}", dll, extraPath);
std::string content = fileutils::text_read(extraPath); std::string content = fileutils::text_read(extraPath);
append_patches(content, apply_patches, nullptr, extraId); append_patches(content, apply_patches, nullptr, extraId);
if (ACTIVE_JSON_FILE.empty()) { if (ACTIVE_JSON_FILE.empty()) {
ACTIVE_JSON_FILE = extraPath.string(); ACTIVE_JSON_FILE = displayPath(extraPath);
} else { } else {
ACTIVE_JSON_FILE += ", " + extraPath.string(); ACTIVE_JSON_FILE += ", " + displayPath(extraPath);
} }
} }
} else { } else {
@@ -3024,22 +3028,23 @@ namespace overlay::windows {
} }
void create_dll_backup( void create_dll_backup(
std::vector<std::string>& written_list, const std::filesystem::path& dll_path) { std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path) {
// if dll_path is not in written_list, create a file backup. // if dll_path is not in written_list, create a file backup.
if (std::find(written_list.begin(), written_list.end(), dll_path.string()) == written_list.end()) { if (std::find(written_list.begin(), written_list.end(), dll_path) == written_list.end()) {
written_list.push_back(dll_path.string()); written_list.push_back(dll_path);
auto dll_bak_path = std::filesystem::path(dll_path.string() + ".bak"); auto dll_bak_path = dll_path;
dll_bak_path += ".bak";
try { try {
if (!fileutils::file_exists(dll_bak_path)) { if (!fileutils::file_exists(dll_bak_path)) {
std::filesystem::copy(dll_path, dll_bak_path); std::filesystem::copy(dll_path, dll_bak_path);
} }
log_info("patchmanager", "created DLL backup for: {}", dll_path.string()); log_info("patchmanager", "created DLL backup for: {}", dll_path);
} catch (const std::filesystem::filesystem_error& e) { } catch (const std::filesystem::filesystem_error& e) {
log_warning( log_warning(
"patchmanager", "patchmanager",
"filesystem error while creating DLL backup for {}, error: {}", "filesystem error while creating DLL backup for {}, error: {}",
dll_path.string(), e.what()); dll_path, e.what());
} }
} }
} }
@@ -3065,14 +3070,14 @@ namespace overlay::windows {
/// check if file exists /// check if file exists
auto dll_path = MODULE_PATH / dll_name; auto dll_path = MODULE_PATH / dll_name;
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
log_warning("patchmanager", "{} does not exist", dll_path.string()); log_warning("patchmanager", "{} does not exist", dll_path);
return nullptr; return nullptr;
} }
// get module // get module
auto module = libutils::try_module(dll_path); auto module = libutils::try_module(dll_path);
if (!module) { if (!module) {
log_warning("patchmanager", "cannot get module: {}", dll_path.string()); log_warning("patchmanager", "cannot get module: {}", dll_path);
return nullptr; return nullptr;
} }
@@ -3081,7 +3086,7 @@ namespace overlay::windows {
if (offset == -1) { if (offset == -1) {
log_warning( log_warning(
"patchmanager", "cannot convert offset to RVA: {}, {}", "patchmanager", "cannot convert offset to RVA: {}, {}",
dll_path.string(), data_offset); dll_path, data_offset);
return nullptr; return nullptr;
} }
@@ -3095,7 +3100,7 @@ namespace overlay::windows {
log_warning( log_warning(
"patchmanager", "GetModuleInformation failed for {}, gle: {}", "patchmanager", "GetModuleInformation failed for {}, gle: {}",
dll_path.string(), GetLastError()); dll_path, GetLastError());
return nullptr; return nullptr;
} }
+2 -2
View File
@@ -162,11 +162,11 @@ namespace overlay::windows {
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size); uint8_t* destination, const std::string& dll_name, size_t offset, size_t size);
void create_dll_backup( void create_dll_backup(
std::vector<std::string>& written_list, const std::filesystem::path& dll_path); std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
std::string fix_up_dll_name(const std::string& dll_name); std::string fix_up_dll_name(const std::string& dll_name);
uint8_t* get_dll_offset_for_patch_apply( uint8_t* get_dll_offset_for_patch_apply(
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes); const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes);
uint64_t parse_json_data_offset( uint64_t parse_json_data_offset(
const std::string &patch_name, const rapidjson::Value &value); const std::string &patch_name, const rapidjson::Value &value);
+30 -19
View File
@@ -9,6 +9,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "external/robin_hood.h" #include "external/robin_hood.h"
#include "util/precise_timer.h"
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
@@ -155,8 +156,9 @@ void rawinput::RawInputManager::input_hwnd_create() {
}); });
// wait for window creation being done // wait for window creation being done
timeutils::PreciseSleepTimer timer;
while (!this->input_hwnd) { while (!this->input_hwnd) {
Sleep(1); timer.sleep(1);
} }
} }
@@ -605,14 +607,22 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
value_caps.LogicalMax = 255; value_caps.LogicalMax = 255;
} }
// fix min and max values // fix up invalid max values (seen on xbox controllers where max is 0xffffffff despite being 16-bit)
if (value_caps.BitSize > 0 && value_caps.BitSize <= sizeof(value_caps.LogicalMin) * 8) { if (value_caps.LogicalMin == 0 && value_caps.BitSize > 0 && value_caps.BitSize < 32) {
auto shift_size = sizeof(value_caps.LogicalMin) * 8 - value_caps.BitSize + 1; const uint32_t field_max = (1u << value_caps.BitSize) - 1u;
auto mask = ((uint64_t) 1 << value_caps.BitSize) - 1; const uint32_t logical_max = static_cast<uint32_t>(value_caps.LogicalMax);
value_caps.LogicalMin &= mask;
value_caps.LogicalMin <<= shift_size; if (logical_max > field_max) {
value_caps.LogicalMin >>= shift_size; log_info(
value_caps.LogicalMax &= mask; "rawinput",
"value cap {} LogicalMax exceeds bit width, fixing it up: {} -> {}",
value_cap_num,
value_caps.LogicalMax,
field_max
);
value_caps.LogicalMax = static_cast<LONG>(field_max);
}
} }
// fix up hat switch to initially report as neutral position // fix up hat switch to initially report as neutral position
@@ -1914,15 +1924,6 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
LONG value_min = value_caps.LogicalMin; LONG value_min = value_caps.LogicalMin;
LONG value_max = value_caps.LogicalMax; LONG value_max = value_caps.LogicalMax;
// fix sign bits for signed values
if (value_caps.LogicalMin < 0 &&
value_caps.BitSize > 0 &&
value_caps.BitSize <= sizeof(value_caps.LogicalMin) * 8) {
auto shift_size = sizeof(value_caps.LogicalMin) * 8 - value_caps.BitSize + 1;
value_raw <<= shift_size;
value_raw >>= shift_size;
}
float value; float value;
// 0x1 == generic desktop, 0x39 == hat switch // 0x1 == generic desktop, 0x39 == hat switch
if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) { if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) {
@@ -1935,6 +1936,16 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
value = -1.f; value = -1.f;
} }
} else { } else {
// fix sign bits for signed values
if (value_caps.LogicalMin < 0 &&
0 < value_caps.BitSize && value_caps.BitSize < 32) {
ULONG raw = static_cast<ULONG>(value_raw) & ((1u << value_caps.BitSize) - 1u);
const ULONG sign_bit = 1u << (value_caps.BitSize - 1);
value_raw = static_cast<LONG>((raw ^ sign_bit) - sign_bit);
}
// automatic calibration // automatic calibration
if (value_raw < value_min) { if (value_raw < value_min) {
value_caps.LogicalMin = value_raw; value_caps.LogicalMin = value_raw;
@@ -2974,4 +2985,4 @@ void rawinput::RawInputManager::remove_callback_midi(void * data, const std::fun
[data, callback](MidiCallback const &cb) { [data, callback](MidiCallback const &cb) {
return cb.data == data && cb.f.target<void>() == callback.target<void>(); return cb.data == data && cb.f.target<void>() == callback.target<void>();
}), this->callback_midi.end()); }), this->callback_midi.end());
} }
+1 -1
View File
@@ -99,7 +99,7 @@ namespace rawinput::touch {
// check type // check type
if (device->type != HID) { if (device->type != HID) {
log_fatal("rawinput", "touch update called on non HID device"); log_warning("rawinput", "touch update called on non HID device");
return; return;
} }
+133 -75
View File
@@ -1,6 +1,11 @@
#include "rawinput/xinput.h" #include "rawinput/xinput.h"
#include "util/logging.h" #include "util/logging.h"
// std::min
#ifdef min
#undef min
#endif
namespace xinput { namespace xinput {
// this is all we need to emulate xinput.h which we avoid including here... // this is all we need to emulate xinput.h which we avoid including here...
@@ -24,6 +29,10 @@ namespace xinput {
#define XINPUT_GAMEPAD_X 0x4000 #define XINPUT_GAMEPAD_X 0x4000
#define XINPUT_GAMEPAD_Y 0x8000 #define XINPUT_GAMEPAD_Y 0x8000
// custom
static constexpr float GAMEPAD_THUMB_DIGITAL_THRESHOLD = 0.2f;
typedef struct { typedef struct {
uint32_t dwPacketNumber; uint32_t dwPacketNumber;
XINPUT_GAMEPAD_STATE Gamepad; XINPUT_GAMEPAD_STATE Gamepad;
@@ -153,7 +162,7 @@ XInputSetState(
float XInputManager::get_analog_state(uint8_t player, XInputAnalogEnum analog) { float XInputManager::get_analog_state(uint8_t player, XInputAnalogEnum analog) {
return 0.5f; return 0.5f;
} }
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) { bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button, XINPUT_GAMEPAD_STATE_NORMALIZED *state_in) {
return false; return false;
} }
bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) { bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) {
@@ -223,6 +232,40 @@ XInputSetState(
return players; return players;
} }
static void normalize_stick(
SHORT raw_x,
SHORT raw_y,
float deadzone,
float &out_x,
float &out_y) {
const int x_i = static_cast<int>(raw_x);
const int y_i = -static_cast<int>(raw_y);
const float x_f = static_cast<float>(x_i);
const float y_f = static_cast<float>(y_i);
const float magnitude = std::sqrt(x_f * x_f + y_f * y_f);
// within deadzone; ignore
if (magnitude <= deadzone) {
out_x = 0.5f;
out_y = 0.5f;
return;
}
// scale value starting with 0 from deadzone border
const float magnitude_clamped = std::min(magnitude, 32767.0f);
const float scaled = (magnitude_clamped - deadzone) / (32767.0f - deadzone);
// normalize
const float normalized_x = (x_f / magnitude) * scaled;
const float normalized_y = (y_f / magnitude) * scaled;
// convert range to [0, 1] with 0.5 as center
out_x = std::clamp((normalized_x + 1.f) / 2.f, 0.f, 1.f);
out_y = std::clamp((normalized_y + 1.f) / 2.f, 0.f, 1.f);
}
void XInputManager::get_state(uint8_t player, XINPUT_GAMEPAD_STATE_NORMALIZED &state) { void XInputManager::get_state(uint8_t player, XINPUT_GAMEPAD_STATE_NORMALIZED &state) {
state = {}; state = {};
state.sThumbLX = 0.5f; state.sThumbLX = 0.5f;
@@ -233,7 +276,7 @@ XInputSetState(
return; return;
} }
XINPUT_STATE x; XINPUT_STATE x = {};
if (XInputGetState_addr(player, &x) != ERROR_SUCCESS) { if (XInputGetState_addr(player, &x) != ERROR_SUCCESS) {
return; return;
} }
@@ -242,106 +285,83 @@ XInputSetState(
state.bLeftTrigger = x.Gamepad.bLeftTrigger / 255.0f; state.bLeftTrigger = x.Gamepad.bLeftTrigger / 255.0f;
state.bRightTrigger = x.Gamepad.bRightTrigger / 255.0f; state.bRightTrigger = x.Gamepad.bRightTrigger / 255.0f;
// left stick circular dead zone // apply circular deadzone logic to left stick
{ normalize_stick(
const auto x_raw = x.Gamepad.sThumbLX; x.Gamepad.sThumbLX,
const auto y_raw = -x.Gamepad.sThumbLY; // flip to make [down = positive] x.Gamepad.sThumbLY,
const float magnitude = sqrtf(x_raw * x_raw + y_raw * y_raw); static_cast<float>(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE),
if (magnitude > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { state.sThumbLX,
const float scaled = state.sThumbLY);
(magnitude - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) /
(32767.0f - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
state.sThumbLX = (x_raw / magnitude) * scaled; // apply circular deadzone logic to right stick
state.sThumbLY = (y_raw / magnitude) * scaled; normalize_stick(
x.Gamepad.sThumbRX,
// normalize to [0, 1] x.Gamepad.sThumbRY,
state.sThumbLX = std::clamp((state.sThumbLX + 1.f) / 2.f, 0.f, 1.f); static_cast<float>(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE),
state.sThumbLY = std::clamp((state.sThumbLY + 1.f) / 2.f, 0.f, 1.f); state.sThumbRX,
} else { state.sThumbRY);
// within deadzone
state.sThumbLX = 0.5f;
state.sThumbLY = 0.5f;
}
}
// right stick circular dead zone
{
const auto x_raw = x.Gamepad.sThumbRX;
const auto y_raw = -x.Gamepad.sThumbRY; // flip to make [down = positive]
const float magnitude = sqrtf(x_raw * x_raw + y_raw * y_raw);
if (magnitude > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) {
const float scaled =
(magnitude - XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) /
(32767.0f - XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
state.sThumbRX = (x_raw / magnitude) * scaled;
state.sThumbRY = (y_raw / magnitude) * scaled;
// normalize to [0, 1]
state.sThumbRX = std::clamp((state.sThumbRX + 1.f) / 2.f, 0.f, 1.f);
state.sThumbRY = std::clamp((state.sThumbRY + 1.f) / 2.f, 0.f, 1.f);
} else {
// within deadzone
state.sThumbRX = 0.5f;
state.sThumbRY = 0.5f;
}
}
} }
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) { bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button, XINPUT_GAMEPAD_STATE_NORMALIZED *state_in) {
XINPUT_GAMEPAD_STATE_NORMALIZED state; XINPUT_GAMEPAD_STATE_NORMALIZED state_on_stack;
XINPUT_GAMEPAD_STATE_NORMALIZED *state;
get_state(player, state); if (state_in) {
state = state_in;
} else {
state = &state_on_stack;
get_state(player, *state);
}
switch (button) { switch (button) {
case XInputButtonEnum::DPAD_UP: case XInputButtonEnum::DPAD_UP:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_UP) != 0; return (state->wButtons & XINPUT_GAMEPAD_DPAD_UP) != 0;
case XInputButtonEnum::DPAD_DOWN: case XInputButtonEnum::DPAD_DOWN:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) != 0; return (state->wButtons & XINPUT_GAMEPAD_DPAD_DOWN) != 0;
case XInputButtonEnum::DPAD_LEFT: case XInputButtonEnum::DPAD_LEFT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) != 0; return (state->wButtons & XINPUT_GAMEPAD_DPAD_LEFT) != 0;
case XInputButtonEnum::DPAD_RIGHT: case XInputButtonEnum::DPAD_RIGHT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0; return (state->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0;
case XInputButtonEnum::START: case XInputButtonEnum::START:
return (state.wButtons & XINPUT_GAMEPAD_START) != 0; return (state->wButtons & XINPUT_GAMEPAD_START) != 0;
case XInputButtonEnum::BACK: case XInputButtonEnum::BACK:
return (state.wButtons & XINPUT_GAMEPAD_BACK) != 0; return (state->wButtons & XINPUT_GAMEPAD_BACK) != 0;
case XInputButtonEnum::LEFT_STICK: case XInputButtonEnum::LEFT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0; return (state->wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0;
case XInputButtonEnum::RIGHT_STICK: case XInputButtonEnum::RIGHT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) != 0; return (state->wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) != 0;
case XInputButtonEnum::LEFT_SHOULDER: case XInputButtonEnum::LEFT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0; return (state->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0;
case XInputButtonEnum::RIGHT_SHOULDER: case XInputButtonEnum::RIGHT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0; return (state->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
case XInputButtonEnum::BUTTON_A: case XInputButtonEnum::BUTTON_A:
return (state.wButtons & XINPUT_GAMEPAD_A) != 0; return (state->wButtons & XINPUT_GAMEPAD_A) != 0;
case XInputButtonEnum::BUTTON_B: case XInputButtonEnum::BUTTON_B:
return (state.wButtons & XINPUT_GAMEPAD_B) != 0; return (state->wButtons & XINPUT_GAMEPAD_B) != 0;
case XInputButtonEnum::BUTTON_X: case XInputButtonEnum::BUTTON_X:
return (state.wButtons & XINPUT_GAMEPAD_X) != 0; return (state->wButtons & XINPUT_GAMEPAD_X) != 0;
case XInputButtonEnum::BUTTON_Y: case XInputButtonEnum::BUTTON_Y:
return (state.wButtons & XINPUT_GAMEPAD_Y) != 0; return (state->wButtons & XINPUT_GAMEPAD_Y) != 0;
case XInputButtonEnum::LEFT_TRIGGER: case XInputButtonEnum::LEFT_TRIGGER:
return state.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; return state->bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::RIGHT_TRIGGER: case XInputButtonEnum::RIGHT_TRIGGER:
return state.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD; return state->bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::LEFT_STICK_UP: case XInputButtonEnum::LEFT_STICK_UP:
return state.sThumbLY > 0.6f; return state->sThumbLY < (0.5f - GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::LEFT_STICK_DOWN: case XInputButtonEnum::LEFT_STICK_DOWN:
return state.sThumbLY < 0.4f; return state->sThumbLY > (0.5f + GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::LEFT_STICK_LEFT: case XInputButtonEnum::LEFT_STICK_LEFT:
return state.sThumbLX < 0.4f; return state->sThumbLX < (0.5f - GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::LEFT_STICK_RIGHT: case XInputButtonEnum::LEFT_STICK_RIGHT:
return state.sThumbLX > 0.6f; return state->sThumbLX > (0.5f + GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::RIGHT_STICK_UP: case XInputButtonEnum::RIGHT_STICK_UP:
return state.sThumbRY > 0.6f; return state->sThumbRY < (0.5f - GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::RIGHT_STICK_DOWN: case XInputButtonEnum::RIGHT_STICK_DOWN:
return state.sThumbRY < 0.4f; return state->sThumbRY > (0.5f + GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::RIGHT_STICK_LEFT: case XInputButtonEnum::RIGHT_STICK_LEFT:
return state.sThumbRX < 0.4f; return state->sThumbRX < (0.5f - GAMEPAD_THUMB_DIGITAL_THRESHOLD);
case XInputButtonEnum::RIGHT_STICK_RIGHT: case XInputButtonEnum::RIGHT_STICK_RIGHT:
return state.sThumbRX > 0.6f; return state->sThumbRX > (0.5f + GAMEPAD_THUMB_DIGITAL_THRESHOLD);
default: default:
break; break;
} }
@@ -374,11 +394,49 @@ XInputSetState(
} }
bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) { bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) {
constexpr std::array<XInputButtonEnum, static_cast<size_t>(XInputButtonEnum::COUNT)> button_priority = {
// the ordering here is important; we want to check buttons first, then dpad, then analog
// this is to help with cases like DDR pads that output multiple at the same time for arrows
// actual buttons
XInputButtonEnum::BUTTON_A,
XInputButtonEnum::BUTTON_B,
XInputButtonEnum::BUTTON_X,
XInputButtonEnum::BUTTON_Y,
XInputButtonEnum::START,
XInputButtonEnum::BACK,
XInputButtonEnum::LEFT_STICK,
XInputButtonEnum::RIGHT_STICK,
XInputButtonEnum::LEFT_SHOULDER,
XInputButtonEnum::RIGHT_SHOULDER,
// dpad
XInputButtonEnum::DPAD_UP,
XInputButtonEnum::DPAD_DOWN,
XInputButtonEnum::DPAD_LEFT,
XInputButtonEnum::DPAD_RIGHT,
// analog values that can be used as buttons
XInputButtonEnum::LEFT_TRIGGER,
XInputButtonEnum::RIGHT_TRIGGER,
XInputButtonEnum::LEFT_STICK_UP,
XInputButtonEnum::LEFT_STICK_DOWN,
XInputButtonEnum::LEFT_STICK_LEFT,
XInputButtonEnum::LEFT_STICK_RIGHT,
XInputButtonEnum::RIGHT_STICK_UP,
XInputButtonEnum::RIGHT_STICK_DOWN,
XInputButtonEnum::RIGHT_STICK_LEFT,
XInputButtonEnum::RIGHT_STICK_RIGHT,
};
for (uint8_t player = 0; player < XUSER_MAX_COUNT; player++) { for (uint8_t player = 0; player < XUSER_MAX_COUNT; player++) {
XINPUT_GAMEPAD_STATE_NORMALIZED state;
get_state(player, state);
for (uint16_t b = 0; b < static_cast<uint16_t>(XInputButtonEnum::COUNT); b++) { for (uint16_t b = 0; b < static_cast<uint16_t>(XInputButtonEnum::COUNT); b++) {
if (is_button_pressed(player, static_cast<XInputButtonEnum>(b))) { if (is_button_pressed(player, button_priority[b], &state)) {
button.player = player; button.player = player;
button.button = static_cast<XInputButtonEnum>(b); button.button = button_priority[b];
return true; return true;
} }
} }

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