mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 | |||
| 2aae8ad9ca | |||
| 1b6d1af951 | |||
| 5c3617c05f | |||
| a9a943fbfe | |||
| b094a9e352 | |||
| 4bd18afb12 | |||
| 0b24e703a1 | |||
| 1a1be7a393 | |||
| a2d8f60d12 | |||
| 5d25fdb035 | |||
| c255e3a1db | |||
| d7c144646f | |||
| 27077aa21f | |||
| 632dcf7b41 | |||
| a25bbdc9c9 | |||
| 47d886306e | |||
| edda658fa4 | |||
| 1ab20f1b56 | |||
| 0e9093a74b | |||
| f52eaea76c | |||
| c3fcf2f417 | |||
| 8d04eba21d | |||
| 8d42c0ce77 | |||
| 829503568e | |||
| 746fdb5409 | |||
| 67da892fe9 | |||
| dc191487e0 | |||
| 5c0c405800 | |||
| 60166e6ba1 | |||
| cb0b59600a | |||
| b9f3be4df2 |
@@ -12,12 +12,12 @@ jobs:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Draft Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Draft Release
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Clean leftover build artifacts
|
||||
run: |
|
||||
rm -rf .ccache dist bin cmake-build-*
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- name: Determine release name from dist filename
|
||||
run: |
|
||||
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||
name="${dist%.zip}"
|
||||
# the tag is the date portion, e.g. 26-06-28
|
||||
tag="${name#spice2x-}"
|
||||
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
target_commitish: main
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
src/spice2x/dist/spice2x-*.zip
|
||||
+7
-3
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
||||
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
||||
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
||||
|
||||
### Forbidden "features" that will never be accepted
|
||||
### Pull requests that will not be accepted
|
||||
|
||||
* Performing decryption of encrypted files
|
||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
|
||||
* Performing decryption of encrypted files.
|
||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
|
||||
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
||||
* This includes automatic updaters.
|
||||
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
||||
* Built-in EA server that saves data. They should live in a separate project.
|
||||
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||
* Changes to the software license terms.
|
||||
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||
|
||||
### Avoiding regressions
|
||||
|
||||
|
||||
@@ -412,10 +412,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/sdvx/io.cpp
|
||||
games/sdvx/camera.cpp
|
||||
games/jb/jb.cpp
|
||||
games/jb/jb_touch.cpp
|
||||
games/jb/io.cpp
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/nost/touch_mode.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
@@ -431,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/rb/rb.cpp
|
||||
games/rb/io.cpp
|
||||
games/rb/touch.cpp
|
||||
games/rb/touch_debug.cpp
|
||||
games/bs/bs.cpp
|
||||
games/bs/io.cpp
|
||||
games/rf3d/rf3d.cpp
|
||||
@@ -500,6 +503,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
hooks/audio/xact.cpp
|
||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
@@ -526,6 +530,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/nvapi_impl.cpp
|
||||
hooks/graphics/nvapi_hook.cpp
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
@@ -569,7 +574,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -607,17 +611,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.cpp
|
||||
overlay/windows/midi.cpp
|
||||
overlay/windows/nostalgia_touch_piano.cpp
|
||||
overlay/windows/obs.cpp
|
||||
overlay/windows/obs_websocket.cpp
|
||||
overlay/windows/patch_manager.cpp
|
||||
overlay/windows/popn_sub.cpp
|
||||
overlay/windows/wnd_manager.cpp
|
||||
|
||||
# patcher
|
||||
patcher/config.cpp
|
||||
patcher/lifecycle.cpp
|
||||
patcher/loader.cpp
|
||||
patcher/loader_group.cpp
|
||||
patcher/runtime.cpp
|
||||
|
||||
# external
|
||||
external/easywsclient/easywsclient.cpp
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/rawinput_handles.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
rawinput/touch.cpp
|
||||
@@ -644,8 +658,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
stubs/stubs.cpp
|
||||
|
||||
# touch
|
||||
touch/gdi_overlay.cpp
|
||||
touch/native/inject.cpp
|
||||
touch/native/inject_mouse.cpp
|
||||
touch/native/inject_synthetic.cpp
|
||||
touch/native/nativetouchhook.cpp
|
||||
touch/native/transform.cpp
|
||||
touch/touch.cpp
|
||||
touch/touch_indicators.cpp
|
||||
touch/touch_gestures.cpp
|
||||
touch/win7.cpp
|
||||
touch/win8.cpp
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
// static stuff
|
||||
static int ACIO_WARMUP = 0;
|
||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
||||
// flush device output
|
||||
RI_MGR->devices_flush_output();
|
||||
|
||||
// update wintouchemu
|
||||
// update touch emulation
|
||||
wintouchemu::update();
|
||||
nativetouch::refresh_contact_lifetime();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/nost/io.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
#include "util/logging.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
||||
// static stuff
|
||||
static uint8_t STATUS_BUFFER[277];
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||
};
|
||||
|
||||
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||
|
||||
// iterate pairs of keys
|
||||
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
||||
|
||||
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button0 > 0) {
|
||||
state0 = button0;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
|
||||
state0 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state0 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
const auto button1 = panb_get_button_velocity(
|
||||
buttons.at(button_mapping[key_pair * 2 + 1]),
|
||||
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button1 > 0) {
|
||||
state1 = button1;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
|
||||
state1 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state1 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
// build value
|
||||
uint8_t value = 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,6 +10,8 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
@@ -17,6 +21,25 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool inject_native_touch(
|
||||
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||
if (touch_points.empty()) {
|
||||
// no active touches remain; release the synthetic contact
|
||||
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||
}
|
||||
|
||||
// only a single synthetic contact is supported, so just use the first touch point
|
||||
const auto &touch = touch_points.front();
|
||||
POINT position { touch.x, touch.y };
|
||||
if (canvas_w > 0 && canvas_h > 0) {
|
||||
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||
position,
|
||||
{ canvas_w, canvas_h },
|
||||
true);
|
||||
}
|
||||
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +49,25 @@ namespace api::modules {
|
||||
is_tdj_fhd = false;
|
||||
}
|
||||
|
||||
use_native = nativetouch::is_hooked();
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||
native_canvas_w = 0;
|
||||
native_canvas_h = 0;
|
||||
if (is_sdvx) {
|
||||
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
||||
native_canvas_w = 1080;
|
||||
native_canvas_h = 1920;
|
||||
} else if (avs::game::is_model("LDJ")) {
|
||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||
} else if (avs::game::is_model("M39")) {
|
||||
// pop'n music API touch surface
|
||||
native_canvas_w = 1280;
|
||||
native_canvas_h = 800;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,7 +141,11 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
touch_write_points(&touch_points);
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +169,43 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
touch_remove_points(&touch_point_ids);
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
touch_remove_points(&touch_point_ids);
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
const auto touch_id = touch_points.front().id;
|
||||
if (native_touch_id && *native_touch_id != touch_id) {
|
||||
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
return;
|
||||
}
|
||||
native_touch_id.reset();
|
||||
}
|
||||
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id = touch_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
if (!native_touch_id ||
|
||||
std::find(
|
||||
touch_point_ids.begin(),
|
||||
touch_point_ids.end(),
|
||||
*native_touch_id) == touch_point_ids.end()) {
|
||||
return;
|
||||
}
|
||||
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::apply_touch_errata(int &x, int &y) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
bool is_tdj_fhd;
|
||||
bool use_native;
|
||||
std::mutex native_touch_mutex;
|
||||
std::optional<uint32_t> native_touch_id;
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||
// used to normalize before mapping into the native injection window
|
||||
int native_canvas_w;
|
||||
int native_canvas_h;
|
||||
|
||||
// function definitions
|
||||
void read(Request &req, Response &res);
|
||||
void write(Request &req, Response &res);
|
||||
void write_reset(Request &req, Response &res);
|
||||
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||
void apply_touch_errata(int &x, int &y);
|
||||
|
||||
};
|
||||
|
||||
@@ -214,6 +214,19 @@ time (
|
||||
echo "==========================="
|
||||
)
|
||||
|
||||
echo ""
|
||||
echo "Checking Win7 compatibility..."
|
||||
echo "============================="
|
||||
if ! command -v windows_dll_compat_checker &> /dev/null; then
|
||||
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
|
||||
else
|
||||
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
|
||||
${BUILDDIR_64}/spicetools/64/spice64.exe
|
||||
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
|
||||
${BUILDDIR_32}/spicetools/spicecfg.exe \
|
||||
${BUILDDIR_32}/spicetools/32/spice.exe
|
||||
fi
|
||||
|
||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||
then
|
||||
echo ""
|
||||
@@ -229,7 +242,7 @@ then
|
||||
fi
|
||||
if ((BUILD_XP_32 > 0))
|
||||
then
|
||||
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
|
||||
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
|
||||
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
||||
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
||||
fi
|
||||
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||
mkdir -p ${OUTDIR_EXTRAS}/updater
|
||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||
then
|
||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
|
||||
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
||||
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
||||
|
||||
# generate the standalone updater scripts from the shared template and drop them
|
||||
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
|
||||
|
||||
# build distribution archive
|
||||
if ((DIST_ENABLE > 0))
|
||||
then
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# builds the standalone updater scripts from the shared PowerShell logic in
|
||||
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
|
||||
# header (with the release channel baked in) followed by the shared PowerShell
|
||||
# body after the marker line.
|
||||
#
|
||||
# usage: build_updaters.sh OUT_DIR
|
||||
# OUT_DIR directory to write the generated .bat files into
|
||||
|
||||
set -eu
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "usage: build_updaters.sh OUT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PS_BODY="${SRC_DIR}/update_spice.ps1"
|
||||
OUT_DIR="$1"
|
||||
|
||||
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
|
||||
|
||||
MARKER='#___PS___'
|
||||
|
||||
gen() {
|
||||
channel="$1"; label="$2"; out="$3"
|
||||
{
|
||||
cat <<HEADER
|
||||
@echo off
|
||||
setlocal
|
||||
REM ============================================================================
|
||||
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
|
||||
REM this folder to the latest spice2x release from GitHub.
|
||||
REM
|
||||
REM release channel: ${label}
|
||||
REM make sure spice/spicecfg are NOT running before updating.
|
||||
REM ============================================================================
|
||||
|
||||
REM expose the script folder + channel to the embedded PowerShell, then run the
|
||||
REM PowerShell part: re-read this file and execute everything after the marker.
|
||||
set "SPICE_DIR=%~dp0"
|
||||
set "SPICE_CHANNEL=${channel}"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
|
||||
set "RC=%ERRORLEVEL%"
|
||||
endlocal & exit /b %RC%
|
||||
|
||||
${MARKER}
|
||||
HEADER
|
||||
cat "$PS_BODY"
|
||||
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
|
||||
echo " generated ${OUT_DIR}/${out}"
|
||||
}
|
||||
|
||||
echo "Generating updater scripts from $(basename "$PS_BODY")..."
|
||||
gen "" "stable (pre-releases excluded)" "update_spice.bat"
|
||||
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
|
||||
+72
-11
@@ -3,7 +3,9 @@
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/piuio.h"
|
||||
#include "util/time.h"
|
||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
||||
namespace GameAPI::Buttons {
|
||||
static State get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &button,
|
||||
bool check_alts,
|
||||
bool check_modifiers);
|
||||
|
||||
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
|
||||
}
|
||||
|
||||
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
|
||||
const auto modifier_mask = button.getModifierMask();
|
||||
if (modifier_mask == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
|
||||
if (!modifier_buttons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
|
||||
(index >= modifier_buttons->size() ||
|
||||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
|
||||
GameAPI::Buttons::BUTTON_PRESSED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &_button,
|
||||
bool check_alts,
|
||||
bool check_modifiers) {
|
||||
|
||||
// check override
|
||||
if (_button.override_enabled) {
|
||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
std::optional<bool> window_has_focus;
|
||||
while (true) {
|
||||
|
||||
// skip bindings whose required modifiers are not held
|
||||
//
|
||||
// note that modifiers cannot process MIDI as the logic is written
|
||||
// below, since MIDI buttons are event-based (on event, off event, etc)
|
||||
// and cannot be correctly handled by a simple early return
|
||||
// there is no explicit check for MIDI here, but the UI should have
|
||||
// prevented it
|
||||
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
|
||||
button_count++;
|
||||
if (!alternatives || alternatives->empty() ||
|
||||
button_count - 1 >= alternatives->size()) {
|
||||
return BUTTON_NOT_PRESSED;
|
||||
}
|
||||
current_button = &alternatives->at(button_count - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (current_button->isNaive()) {
|
||||
GameAPI::Buttons::State state;
|
||||
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
}
|
||||
case rawinput::DESTROYED:
|
||||
// device was unplugged. release the button instead of leaving
|
||||
// state at getLastState(), which would latch a held button on
|
||||
// forever (e.g. a MIDI note held while the device is removed)
|
||||
state = BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -463,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
|
||||
return get_button_state(manager, button, check_alts, true);
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
||||
if (manager) {
|
||||
return getState(manager.get(), button, check_alts);
|
||||
@@ -478,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
return button.override_velocity;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (button.isNaive()) {
|
||||
if (button.getInvert()) {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
||||
} else {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// get button state
|
||||
Buttons::State button_state = Buttons::getState(manager, button, false);
|
||||
const auto button_state = Buttons::getState(manager, button, false);
|
||||
|
||||
// naive bindings report their digital state as full or zero velocity
|
||||
if (button.isNaive()) {
|
||||
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
|
||||
}
|
||||
|
||||
// check if button isn't being pressed
|
||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||
|
||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
||||
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
||||
}
|
||||
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
|
||||
auto binding_display = this->getBindingDisplayString(manager);
|
||||
auto modifiers = this->getModifierMask();
|
||||
if (binding_display.empty() || modifiers == 0) {
|
||||
return binding_display;
|
||||
}
|
||||
|
||||
std::string modifier_display;
|
||||
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
|
||||
if ((modifiers & UINT8_C(1)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!modifier_display.empty()) {
|
||||
modifier_display += "+";
|
||||
}
|
||||
modifier_display += fmt::format("Mod{}", index);
|
||||
}
|
||||
return modifier_display + "+" + binding_display;
|
||||
}
|
||||
|
||||
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
|
||||
|
||||
// get VKey string
|
||||
auto vKey = (uint16_t) this->getVKey();
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
std::string name;
|
||||
std::string device_identifier = "";
|
||||
unsigned short vKey = INVALID_VKEY;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
// default bindings are always naive
|
||||
unsigned short vKey_default = INVALID_VKEY;
|
||||
@@ -65,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -123,6 +126,14 @@ public:
|
||||
return this->name;
|
||||
}
|
||||
|
||||
inline uint8_t getModifierMask() const {
|
||||
return this->modifier_mask;
|
||||
}
|
||||
|
||||
inline void setModifierMask(uint8_t new_modifier_mask) {
|
||||
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
|
||||
}
|
||||
|
||||
inline const std::string &getDeviceIdentifier() const {
|
||||
return this->device_identifier;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||
gameButtonNode = this->configFile.NewElement("button");
|
||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
} else {
|
||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
|
||||
// find alternative
|
||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
alt.setDeviceIdentifier(std::string(devid));
|
||||
}
|
||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("bat_threshold", &bat);
|
||||
entry.bat_threshold = bat;
|
||||
|
||||
unsigned int modifier_mask = 0;
|
||||
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -160,6 +165,28 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
|
||||
if (mod_el) {
|
||||
auto *btn_el = mod_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.modifier_buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.modifier_buttons.push_back(
|
||||
{btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights) {
|
||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
||||
}
|
||||
tmpl_el->InsertEndChild(buttons_el);
|
||||
|
||||
// modifier buttons - skip unbound entries
|
||||
if (!tmpl.modifier_buttons.empty()) {
|
||||
auto *mod_el = doc.NewElement("modifier_buttons");
|
||||
if (save_modifiers) {
|
||||
for (auto &btn : tmpl.modifier_buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(mod_el);
|
||||
}
|
||||
|
||||
// keypad buttons — skip unbound entries
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "trackball.h"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
@@ -653,9 +653,7 @@ namespace games::gitadora {
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "launcher/options.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
@@ -63,7 +62,6 @@ namespace games::iidx {
|
||||
bool TDJ_MODE = false;
|
||||
bool FORCE_720P = false;
|
||||
bool DISABLE_ESPEC_IO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
@@ -350,13 +348,7 @@ namespace games::iidx {
|
||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||
devicehook_init(avs::core::DLL_INSTANCE);
|
||||
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
// insert BI2X hooks
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace games::iidx {
|
||||
extern bool TDJ_MODE;
|
||||
extern bool FORCE_720P;
|
||||
extern bool DISABLE_ESPEC_IO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern uint8_t DIGITAL_TT_SENS;
|
||||
|
||||
+19
-144
@@ -3,30 +3,15 @@
|
||||
#include <thread>
|
||||
|
||||
#include "windows.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
#include "games/io.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/windows/generic_sub.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
#define POKE_NATIVE_TOUCH 0
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
|
||||
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
|
||||
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
|
||||
static InjectTouchInput_t pInjectTouchInput = nullptr;
|
||||
#endif
|
||||
|
||||
namespace games::iidx::poke {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
@@ -116,80 +101,13 @@ namespace games::iidx::poke {
|
||||
{"1/D", 50},
|
||||
};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
void initialize_native_touch_library() {
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||
}
|
||||
|
||||
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
|
||||
USER32_INSTANCE, "InitializeTouchInjection");
|
||||
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
|
||||
USER32_INSTANCE, "InjectTouchInput");
|
||||
}
|
||||
|
||||
void emulate_native_touch(TouchPoint tp, bool is_down) {
|
||||
if (pInjectTouchInput == nullptr) {
|
||||
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINTER_TOUCH_INFO contact;
|
||||
BOOL bRet = TRUE;
|
||||
const int contact_offset = 2;
|
||||
|
||||
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
|
||||
|
||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
||||
contact.pointerInfo.pointerId = 0;
|
||||
contact.pointerInfo.ptPixelLocation.x = tp.x;
|
||||
contact.pointerInfo.ptPixelLocation.y = tp.y;
|
||||
if (is_down) {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
||||
} else {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
|
||||
}
|
||||
|
||||
contact.pointerInfo.dwTime = 0;
|
||||
contact.pointerInfo.PerformanceCount = 0;
|
||||
|
||||
contact.touchFlags = TOUCH_FLAG_NONE;
|
||||
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
|
||||
contact.orientation = 0;
|
||||
contact.pressure = 1024;
|
||||
|
||||
contact.rcContact.top = tp.y - contact_offset;
|
||||
contact.rcContact.bottom = tp.y + contact_offset;
|
||||
contact.rcContact.left = tp.x - contact_offset;
|
||||
contact.rcContact.right = tp.x + contact_offset;
|
||||
|
||||
bRet = InjectTouchInput(1, &contact);
|
||||
if (!bRet) {
|
||||
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
int i = 0;
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, true);
|
||||
}
|
||||
}
|
||||
|
||||
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, false);
|
||||
}
|
||||
touch_points->clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
const auto &touch = touch_points.front();
|
||||
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||
}
|
||||
|
||||
void enable() {
|
||||
@@ -210,35 +128,13 @@ namespace games::iidx::poke {
|
||||
std::vector<TouchPoint> touch_points;
|
||||
std::vector<uint16_t> last_keypad_state = {0, 0};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
bool use_native = games::iidx::NATIVE_TOUCH;
|
||||
|
||||
if (use_native) {
|
||||
initialize_native_touch_library();
|
||||
|
||||
if (pInitializeTouchInjection != nullptr) {
|
||||
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
bool use_native = false;
|
||||
#endif
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
|
||||
// clean up touch from last frame
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
clear_native_touch_points(&touch_points);
|
||||
} else {
|
||||
clear_touch_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
clear_touch_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, false);
|
||||
touch_points.clear();
|
||||
}
|
||||
|
||||
for (int unit = 0; unit < 2; unit++) {
|
||||
@@ -263,32 +159,15 @@ namespace games::iidx::poke {
|
||||
|
||||
float x = x_iter->second / 1920.0;
|
||||
float y = y_iter->second / 1080.0;
|
||||
if (use_native) {
|
||||
x *= rawinput::TOUCHSCREEN_RANGE_X;
|
||||
y *= rawinput::TOUCHSCREEN_RANGE_Y;
|
||||
} else if (GRAPHICS_IIDX_WSUB) {
|
||||
// Scale to windowed subscreen
|
||||
x *= GRAPHICS_WSUB_WIDTH;
|
||||
y *= GRAPHICS_WSUB_HEIGHT;
|
||||
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
|
||||
// Overlay is not present, scale to main screen
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
x *= ImGui::GetIO().DisplaySize.x;
|
||||
y *= ImGui::GetIO().DisplaySize.y;
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
continue;
|
||||
}
|
||||
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
// Overlay subscreen
|
||||
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
|
||||
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
|
||||
|
||||
// Scale to window size ratio
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
|
||||
}
|
||||
x = x_iter->second;
|
||||
y = y_iter->second;
|
||||
}
|
||||
|
||||
TouchPoint tp {
|
||||
@@ -309,21 +188,17 @@ namespace games::iidx::poke {
|
||||
} // for all units
|
||||
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
emulate_native_touch_points(&touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
touch_write_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, true);
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(50);
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
inject_native_touch_points(touch_points, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace games {
|
||||
static bool IO_INITIALIZED = false;
|
||||
static std::vector<std::string> games;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
|
||||
static robin_hood::unordered_map<std::string, std::string> buttons_help;
|
||||
@@ -365,6 +366,37 @@ namespace games {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
|
||||
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
|
||||
const std::vector<std::string> names {
|
||||
"Modifier 1",
|
||||
"Modifier 2",
|
||||
"Modifier 3",
|
||||
"Modifier 4",
|
||||
};
|
||||
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
|
||||
for (auto &modifier : modifier_buttons) {
|
||||
modifier.setModifierMask(0);
|
||||
for (auto &alternative : modifier.getAlternatives()) {
|
||||
alternative.setModifierMask(0);
|
||||
}
|
||||
}
|
||||
return modifier_buttons;
|
||||
}
|
||||
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
|
||||
initialize();
|
||||
auto it = buttons_modifiers.find(game);
|
||||
if (it == buttons_modifiers.end()) {
|
||||
if (game.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
buttons_modifiers[game] = gen_buttons_modifiers(game);
|
||||
return &buttons_modifiers[game];
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
|
||||
auto buttons = GameAPI::Buttons::getButtons(game);
|
||||
std::vector<std::string> names;
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
namespace games {
|
||||
|
||||
namespace ModifierButtons {
|
||||
enum {
|
||||
Modifier1,
|
||||
Modifier2,
|
||||
Modifier3,
|
||||
Modifier4,
|
||||
Size,
|
||||
};
|
||||
}
|
||||
|
||||
namespace OverlayButtons {
|
||||
enum {
|
||||
Screenshot,
|
||||
@@ -59,6 +69,7 @@ namespace games {
|
||||
|
||||
const std::vector<std::string> &get_games();
|
||||
std::vector<Button> *get_buttons(const std::string &game);
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game);
|
||||
std::string get_buttons_help(const std::string &game);
|
||||
std::string get_analogs_help(const std::string &game);
|
||||
std::vector<Button> *get_buttons_keypads(const std::string &game);
|
||||
|
||||
+5
-210
@@ -3,212 +3,24 @@
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_BUTTON_GAP 37
|
||||
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch stuff
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
static bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::vector<TouchPoint> TOUCH_POINTS;
|
||||
bool TOUCH_STATE[16];
|
||||
|
||||
void touch_update() {
|
||||
|
||||
// check if touch enabled
|
||||
if (!TOUCH_ENABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch module
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
/*
|
||||
* Find the game window.
|
||||
* We check the foreground window first, then fall back to searching for the window title
|
||||
* All game versions seem to have their model first in the window title
|
||||
*/
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
|
||||
// check if we have a window handle
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch hook
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// show cursor
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
// earlier games use a different screen orientation
|
||||
if (!avs::game::is_model("L44")) {
|
||||
IS_PORTRAIT = false;
|
||||
}
|
||||
|
||||
// set attached
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// reset touch state
|
||||
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
|
||||
|
||||
// check touch points
|
||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
||||
TOUCH_POINTS.clear();
|
||||
touch_get_points(TOUCH_POINTS);
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
|
||||
// get grid coordinates
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
|
||||
// set the corresponding state
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
// check window out of bounds
|
||||
if (IS_PORTRAIT) {
|
||||
if (tp.x > 768 || tp.y > 1360) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (tp.x > 1360 || tp.y > 768) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int x_relative = tp.x;
|
||||
int y_relative = tp.y;
|
||||
|
||||
// x_relative and y_relative are relative to the top-left pixel of the first button
|
||||
if (IS_PORTRAIT) {
|
||||
// which is at (8, 602) in portrait:
|
||||
// X: [8...759] (752 pixels wide)
|
||||
// Y: [602...1353] (752 pixels high)
|
||||
x_relative -= 8;
|
||||
y_relative -= 602;
|
||||
} else {
|
||||
// and at (8, 8) in landscape
|
||||
x_relative -= 8;
|
||||
y_relative -= 8;
|
||||
}
|
||||
|
||||
int x_index = -1;
|
||||
int x_hitbox = 0;
|
||||
int y_index = -1;
|
||||
int y_hitbox = 0;
|
||||
|
||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
||||
if (x_relative >= 0) {
|
||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
if (y_relative >= 0) {
|
||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
|
||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
||||
|
||||
if (TOUCH_ALGORITHM == Improved) {
|
||||
if (IS_PORTRAIT) {
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// right and bottom borders are covered by the hit box
|
||||
} else {
|
||||
// extend to top border
|
||||
if (y_relative < 0) {
|
||||
y_index = 0;
|
||||
}
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// bottom border is covered by the hit box
|
||||
// rightmost edge - ignore them entirely
|
||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the gap was touched
|
||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
||||
// in ac-accurate mode, touching the gap is ignored
|
||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (TOUCH_ALGORITHM == Improved) {
|
||||
// in improved mode, touching the gap triggers the closest button
|
||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
||||
x_index++;
|
||||
}
|
||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
||||
y_index++;
|
||||
}
|
||||
}
|
||||
|
||||
// set the corresponding state
|
||||
int index = y_index * 4 + x_index;
|
||||
if (0 <= index && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
|
||||
*/
|
||||
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||
static BOOL __stdcall network_addr_is_changed() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
|
||||
*/
|
||||
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
|
||||
static BOOL __stdcall network_get_network_check_info() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix network error on non DHCP interfaces
|
||||
*/
|
||||
// fixes network errors on non-DHCP interfaces
|
||||
static BOOL __cdecl network_get_dhcp_result() {
|
||||
return 1;
|
||||
}
|
||||
@@ -248,23 +60,7 @@ namespace games::jb {
|
||||
void JBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
// enable touch
|
||||
TOUCH_ENABLE = true;
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
||||
break;
|
||||
}
|
||||
touch_attach();
|
||||
|
||||
// enable debug logging of gftools
|
||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||
@@ -284,7 +80,6 @@ namespace games::jb {
|
||||
void JBGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
// disable touch
|
||||
TOUCH_ENABLE = false;
|
||||
touch_detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "games/game.h"
|
||||
#include "games/jb/jb_touch.h"
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// touch stuff
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern bool TOUCH_STATE[16];
|
||||
void touch_update();
|
||||
|
||||
class JBGame : public games::Game {
|
||||
public:
|
||||
JBGame();
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
#include "jb_touch.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
|
||||
// (752px across). the first button's top-left is at (8, 602) in portrait and
|
||||
// (6, 8) in landscape.
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_MAX_BUTTON_GAP 38
|
||||
|
||||
// improved and plus modes use this reach around each button. must be >= the
|
||||
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||
#define JB_TOUCH_RADIUS 38
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
static_assert(std::atomic_bool::is_always_lock_free);
|
||||
static_assert(std::atomic_uint16_t::is_always_lock_free);
|
||||
static_assert(std::atomic<POINT>::is_always_lock_free);
|
||||
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
|
||||
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
|
||||
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
|
||||
|
||||
// touch state
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
|
||||
uint32_t TOUCH_DEBOUNCE_MS = 0;
|
||||
static std::atomic_bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||
|
||||
// fixed-size contact view used by the debug overlay
|
||||
static const size_t JB_MAX_TOUCH_POINTS = 16;
|
||||
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
|
||||
static constexpr POINT JB_INVALID_TOUCH_POINT {
|
||||
JB_INVALID_TOUCH_COORD,
|
||||
JB_INVALID_TOUCH_COORD
|
||||
};
|
||||
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
|
||||
|
||||
static void clear_touch_points() {
|
||||
for (auto &point : TOUCH_POINTS) {
|
||||
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// false when a contact is younger than the debounce window and should be ignored
|
||||
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
|
||||
return now_ms - tp.down_ms >= threshold_ms;
|
||||
}
|
||||
|
||||
// snapshot for the debug overlay: each slot atomically holds one matured contact or
|
||||
// the invalid sentinel. immature and absent contacts are published as invalid, so the
|
||||
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
|
||||
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
|
||||
double now_ms, double threshold_ms) {
|
||||
size_t count = touch_points.size();
|
||||
if (count > JB_MAX_TOUCH_POINTS) {
|
||||
count = JB_MAX_TOUCH_POINTS;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
|
||||
POINT point = JB_INVALID_TOUCH_POINT;
|
||||
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
|
||||
point = { touch_points[i].x, touch_points[i].y };
|
||||
}
|
||||
TOUCH_POINTS[i].store(point, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// --- touch geometry ------------------------------------------------------
|
||||
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||
|
||||
struct AxisGeometry {
|
||||
int button[4]; // left/top edge of each button
|
||||
};
|
||||
|
||||
// left/top edges of the four buttons along one axis, starting at `first`
|
||||
static AxisGeometry axis_geometry(int first) {
|
||||
AxisGeometry g {};
|
||||
g.button[0] = first;
|
||||
for (int i = 1; i < 4; i++) {
|
||||
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// button edges for the current orientation
|
||||
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
||||
if (IS_PORTRAIT) {
|
||||
gx = axis_geometry(8);
|
||||
gy = axis_geometry(602);
|
||||
} else {
|
||||
gx = axis_geometry(6);
|
||||
gy = axis_geometry(8);
|
||||
}
|
||||
}
|
||||
|
||||
// distance from `p` to a button along one axis (0 when inside)
|
||||
static int axis_distance(int p, int button) {
|
||||
int end = button + JB_BUTTON_SIZE - 1;
|
||||
if (p < button) {
|
||||
return button - p;
|
||||
}
|
||||
if (p > end) {
|
||||
return p - end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
|
||||
// shared by detection and the debug overlay so both agree which button a touch hits
|
||||
static int nearest_button(
|
||||
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
|
||||
int best_index = -1;
|
||||
int best_dist = 0;
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
int dist = dx * dx + dy * dy;
|
||||
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||
best_dist = dist;
|
||||
best_index = r * 4 + c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
// detection reach for the current algorithm (0 = register only inside a button)
|
||||
static int touch_radius() {
|
||||
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
|
||||
}
|
||||
|
||||
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||
// every button within `radius` when `multi` (edge/gap presses trigger several)
|
||||
static void mark_buttons(uint16_t &state, int px, int py,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
int radius, bool multi) {
|
||||
if (!multi) {
|
||||
int index = nearest_button(px, py, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
state |= uint16_t(1) << index;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
if (dx * dx + dy * dy <= radius * radius) {
|
||||
state |= uint16_t(1) << (r * 4 + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::bitset<16> touch_state() {
|
||||
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
void touch_update() {
|
||||
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// one-time touch window attach
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
// find the game window: prefer the foreground window, else search by
|
||||
// title (the model name prefixes the window title in every version)
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
// only the L44 model runs in portrait; set this before starting the
|
||||
// touch-window thread so the renderer only observes the final value
|
||||
IS_PORTRAIT = avs::game::is_model("L44");
|
||||
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
|
||||
// let the rawinput stack correct the aspect ratio
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// calculate the next state locally and publish it after processing every touch
|
||||
uint16_t next_state = 0;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
touch_get_points(touch_points);
|
||||
|
||||
// debounce: the landing time is stamped by the touch layer, so this is
|
||||
// independent of how often the game polls for input (0 = off)
|
||||
double now_ms = get_performance_milliseconds();
|
||||
double threshold_ms = TOUCH_DEBOUNCE_MS;
|
||||
|
||||
// publish every raw contact (with its maturity) for the debug overlay, then drop
|
||||
// the ones still inside the debounce window so no algorithm sees them
|
||||
publish_touch_points(touch_points, now_ms, threshold_ms);
|
||||
std::erase_if(touch_points, [&](const TouchPoint &tp) {
|
||||
return !touch_matured(tp, now_ms, threshold_ms);
|
||||
});
|
||||
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
|
||||
// legacy: evenly divide the play area into a 4x4 grid
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : touch_points) {
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
next_state |= uint16_t(1) << index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// accurate registers only a touch inside a button; improved snaps each touch
|
||||
// to the single nearest button within reach (so a gap or centre touch still
|
||||
// triggers exactly one button); plus marks every button within the same reach,
|
||||
// so an edge or gap touch can trigger several at once (like the mobile game)
|
||||
AxisGeometry gx, gy;
|
||||
touch_geometry(gx, gy);
|
||||
int radius = touch_radius();
|
||||
bool multi = (TOUCH_ALGORITHM == Plus);
|
||||
for (auto &tp : touch_points) {
|
||||
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
|
||||
}
|
||||
}
|
||||
|
||||
TOUCH_STATE.store(next_state, std::memory_order_release);
|
||||
}
|
||||
|
||||
// true when any supported touch handler detects a touchscreen (checked once)
|
||||
static bool touchscreen_detected() {
|
||||
static const bool detected = is_touch_available("jubeat touch debug overlay");
|
||||
return detected;
|
||||
}
|
||||
|
||||
// auto draws the boxes only when a touch screen is present
|
||||
static bool debug_show_boxes() {
|
||||
switch (TOUCH_DEBUG_OVERLAY) {
|
||||
case JbTouchDebugBox:
|
||||
case JbTouchDebugAll:
|
||||
return true;
|
||||
case JbTouchDebugAuto:
|
||||
return touchscreen_detected();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool debug_show_taps() {
|
||||
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
|
||||
}
|
||||
|
||||
bool touch_debug_overlay_enabled() {
|
||||
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
|
||||
(debug_show_boxes() || debug_show_taps());
|
||||
}
|
||||
|
||||
// the 16 boundary rects: legacy divides the area evenly, others use button squares
|
||||
static void debug_cells(
|
||||
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
|
||||
if (legacy) {
|
||||
int offset = IS_PORTRAIT ? 580 : 0;
|
||||
int x_edges[5];
|
||||
int y_edges[5];
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
x_edges[i] = i * 768 / 4;
|
||||
y_edges[i] = offset + i * (1360 - 580) / 4;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = {
|
||||
gx.button[c], gy.button[r],
|
||||
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
|
||||
static void draw_debug_boxes(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
RECT cells[16];
|
||||
debug_cells(legacy, gx, gy, cells);
|
||||
|
||||
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
|
||||
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
|
||||
auto state = touch_state();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
|
||||
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
|
||||
SelectObject(hdc, old_pen);
|
||||
}
|
||||
DeleteObject(pen_idle);
|
||||
DeleteObject(pen_active);
|
||||
}
|
||||
|
||||
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
|
||||
// reaches across gaps to nearby buttons, and the others only fire inside a button square
|
||||
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
bool legacy, int radius) {
|
||||
if (legacy) {
|
||||
return true;
|
||||
}
|
||||
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
|
||||
return nearest_button(px, py, gx, gy, reach) >= 0;
|
||||
}
|
||||
|
||||
// 90-degree arc on the circle rim facing the centre of button `index`
|
||||
static void draw_tap_arc(HDC hdc, int px, int py, int index,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||
int c = index % 4;
|
||||
int r = index / 4;
|
||||
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
||||
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
||||
const double quarter = 3.14159265358979323846 / 2.0;
|
||||
const int segments = 16;
|
||||
POINT arc[segments + 1];
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
double a = mid - quarter / 2.0 + quarter * i / segments;
|
||||
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
|
||||
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
|
||||
}
|
||||
Polyline(hdc, arc, segments + 1);
|
||||
}
|
||||
|
||||
// circle at each touch: white when it presses a button, grey otherwise; in single-button
|
||||
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
|
||||
// plus, where a touch can trigger several buttons at once)
|
||||
static void draw_debug_taps(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
// PS_INSIDEFRAME keeps the stroke inside the circle radius
|
||||
const int stroke = 4;
|
||||
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
|
||||
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
|
||||
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
|
||||
|
||||
// accurate has no reach, so fall back to a visible marker size when drawing the circle
|
||||
int radius = touch_radius();
|
||||
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
|
||||
|
||||
// each slot holds a matured contact or the invalid sentinel; immature and absent
|
||||
// contacts were already filtered out by publish_touch_points
|
||||
for (auto &touch_point : TOUCH_POINTS) {
|
||||
POINT point = touch_point.load(std::memory_order_acquire);
|
||||
if (point.x == JB_INVALID_TOUCH_COORD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
|
||||
|
||||
SelectObject(hdc, pressed ? pen_white : pen_gray);
|
||||
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
|
||||
point.x + draw_radius, point.y + draw_radius);
|
||||
|
||||
// the arc points at a single snapped-to button; plus can trigger several at
|
||||
// once, so skip it there
|
||||
if (pressed || TOUCH_ALGORITHM == Plus) {
|
||||
continue;
|
||||
}
|
||||
int index = nearest_button(point.x, point.y, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
SelectObject(hdc, pen_white);
|
||||
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SelectObject(hdc, old_pen);
|
||||
DeleteObject(pen_white);
|
||||
DeleteObject(pen_gray);
|
||||
}
|
||||
|
||||
void touch_draw_debug_overlay(HDC hdc) {
|
||||
|
||||
// only draw while touch is active
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool show_boxes = debug_show_boxes();
|
||||
bool show_taps = debug_show_taps();
|
||||
if (!show_boxes && !show_taps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// legacy divides the field evenly; the other algorithms use button squares
|
||||
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
||||
AxisGeometry gx {}, gy {};
|
||||
if (!legacy) {
|
||||
touch_geometry(gx, gy);
|
||||
}
|
||||
|
||||
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
|
||||
if (show_boxes) {
|
||||
draw_debug_boxes(hdc, legacy, gx, gy);
|
||||
}
|
||||
if (show_taps) {
|
||||
draw_debug_taps(hdc, legacy, gx, gy);
|
||||
}
|
||||
SelectObject(hdc, old_brush);
|
||||
}
|
||||
|
||||
void touch_attach() {
|
||||
clear_touch_points();
|
||||
TOUCH_ENABLE.store(true, std::memory_order_release);
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case Plus:
|
||||
log_info("jubeat", "using 'plus' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo, this is a bug");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void touch_detach() {
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch detection algorithm
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
Plus,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// debug touch overlay mode
|
||||
enum JubeatTouchDebugMode {
|
||||
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
|
||||
JbTouchDebugNone, // draw nothing
|
||||
JbTouchDebugBox, // boundary boxes
|
||||
JbTouchDebugAll, // boundary boxes and touch circles
|
||||
};
|
||||
|
||||
// selected algorithm and debug mode (set from the launcher options)
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
|
||||
|
||||
// minimum contact age in milliseconds before a touch registers (0 = off);
|
||||
// filters momentary phantom touches from noisy panels
|
||||
extern uint32_t TOUCH_DEBOUNCE_MS;
|
||||
|
||||
// atomically published panel state, read by the I/O layer
|
||||
std::bitset<16> touch_state();
|
||||
|
||||
// read the current touch points and refresh the panel state
|
||||
void touch_update();
|
||||
|
||||
// enable/disable touch handling on game attach/detach
|
||||
void touch_attach();
|
||||
void touch_detach();
|
||||
|
||||
// whether the debug overlay should currently draw anything
|
||||
bool touch_debug_overlay_enabled();
|
||||
|
||||
// draw the debug touch overlay (boundary boxes and/or touch circles per the
|
||||
// selected mode) onto the spice touch overlay window's device context
|
||||
void touch_draw_debug_overlay(HDC hdc);
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2a_hook.h"
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "nost.h"
|
||||
#include "poke.h"
|
||||
#include "touch_mode.h"
|
||||
#include "hooks/setupapihook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
namespace games::nost {
|
||||
|
||||
bool ENABLE_POKE = false;
|
||||
bool ENABLE_TOUCH_MODE = false;
|
||||
|
||||
NostGame::NostGame() : Game("Nostalgia") {
|
||||
}
|
||||
@@ -43,9 +41,10 @@ namespace games::nost {
|
||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||
setupapihook_add(touch_settings);
|
||||
|
||||
// custom touch
|
||||
// nostalgia crashes if you inject touch events too early
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::enable();
|
||||
}
|
||||
}
|
||||
|
||||
void NostGame::post_attach() {
|
||||
@@ -55,6 +54,9 @@ namespace games::nost {
|
||||
}
|
||||
|
||||
void NostGame::detach() {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::disable();
|
||||
}
|
||||
if (ENABLE_POKE) {
|
||||
poke::disable();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace games::nost {
|
||||
|
||||
extern bool ENABLE_POKE;
|
||||
extern bool ENABLE_TOUCH_MODE;
|
||||
|
||||
class NostGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "games/nost/io.h"
|
||||
#include "cfg/api.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
namespace games::nost::poke {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
static volatile bool THREAD_RUNNING = false;
|
||||
static std::atomic<bool> THREAD_RUNNING { false };
|
||||
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
||||
// positions use game-client pixels, matching the legacy client-sized touch window
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
|
||||
{EAM_IO_KEYPAD_0, {760, 440}},
|
||||
{EAM_IO_KEYPAD_1, {760, 385}},
|
||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||
@@ -31,7 +33,8 @@ namespace games::nost::poke {
|
||||
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
||||
};
|
||||
|
||||
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
|
||||
static const std::unordered_map<
|
||||
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
|
||||
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
||||
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||
@@ -46,13 +49,8 @@ namespace games::nost::poke {
|
||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||
};
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
static bool inject_poke_position(POINT position, bool down) {
|
||||
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||
}
|
||||
|
||||
void enable() {
|
||||
@@ -65,124 +63,138 @@ namespace games::nost::poke {
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||
|
||||
const int swipe_anim_total_frames = 6;
|
||||
const int swipe_anim_y = 300;
|
||||
|
||||
const int swipe_next_page_x_begin = 1120;
|
||||
const int swipe_next_page_x_end = 1120-120;
|
||||
const int swipe_next_page_x_end = 1120 - 120;
|
||||
|
||||
const int swipe_prev_page_x_begin = 280-120;
|
||||
const int swipe_prev_page_x_begin = 280 - 120;
|
||||
const int swipe_prev_page_x_end = 280;
|
||||
|
||||
int next_page_anim_index = -1;
|
||||
int prev_page_anim_index = -1;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
|
||||
bool touch_release = false;
|
||||
bool contact_active = false;
|
||||
bool release_pending = false;
|
||||
POINT last_position {};
|
||||
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||
uint16_t last_keypad_state = 0;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
// clean up touch from last frame
|
||||
if (!touch_points.empty()) {
|
||||
if (touch_release) {
|
||||
clear_touch_points(&touch_points);
|
||||
touch_release = false;
|
||||
} else {
|
||||
touch_points.clear();
|
||||
if (release_pending) {
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
contact_active = false;
|
||||
release_pending = false;
|
||||
}
|
||||
|
||||
auto &buttons = games::nost::get_buttons();
|
||||
std::optional<POINT> touch_position;
|
||||
bool release_after_touch = false;
|
||||
|
||||
const auto button_triggered = [&](games::nost::Buttons::Button button) {
|
||||
const auto pressed =
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
|
||||
const auto triggered = pressed && !button_states[button];
|
||||
button_states[button] = pressed;
|
||||
return triggered;
|
||||
};
|
||||
|
||||
std::optional<POINT> triggered_touch_position;
|
||||
for (const auto& it : NOST_POKE_BUTTONS) {
|
||||
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const auto next_triggered =
|
||||
button_triggered(games::nost::Buttons::PokeNextPage);
|
||||
const auto prev_triggered =
|
||||
button_triggered(games::nost::Buttons::PokePrevPage);
|
||||
|
||||
const auto keypad_state = eamuse_get_keypad_state(0);
|
||||
if (!triggered_touch_position.has_value()) {
|
||||
for (const auto& it : NOST_POKE_KEYPAD) {
|
||||
const auto mask = static_cast<uint16_t>(1 << it.first);
|
||||
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_keypad_state = keypad_state;
|
||||
|
||||
if (0 <= next_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_next_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
next_page_anim_index--;
|
||||
if (next_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else if (0 <= prev_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_prev_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
prev_page_anim_index--;
|
||||
if (prev_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& it : NOST_POKE) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!touch_release) {
|
||||
const auto state = eamuse_get_keypad_state(0);
|
||||
if (state) {
|
||||
for (const auto& it : NOST_POKE_NUM) {
|
||||
if (state & (1 << it.first)) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
touch_position = triggered_touch_position;
|
||||
release_after_touch = touch_position.has_value();
|
||||
|
||||
// start animations for next frame
|
||||
if (!touch_release) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
||||
if (!touch_position.has_value()) {
|
||||
if (next_triggered) {
|
||||
next_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
|
||||
|
||||
if (prev_triggered) {
|
||||
prev_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
touch_write_points(&touch_points);
|
||||
if (touch_position.has_value() &&
|
||||
inject_poke_position(*touch_position, true)) {
|
||||
contact_active = true;
|
||||
last_position = *touch_position;
|
||||
}
|
||||
if (release_after_touch && contact_active) {
|
||||
release_pending = true;
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(30);
|
||||
}
|
||||
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "touch_mode.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// native contact positions feed piano input directly. native events reach the game
|
||||
// in nav mode and are suppressed in piano mode. mode-button contacts are always
|
||||
// suppressed and change that routing after release.
|
||||
static constexpr LONG PIANO_LEFT_GAP = 11;
|
||||
static constexpr LONG PIANO_RIGHT_GAP = 10;
|
||||
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||
|
||||
struct TouchGeometry {
|
||||
HWND window = nullptr;
|
||||
RECT mode_button {};
|
||||
LONG client_width = 0;
|
||||
LONG client_height = 0;
|
||||
|
||||
bool valid() const {
|
||||
return window != nullptr && client_width > 0 && client_height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct NativeContact {
|
||||
POINT position {};
|
||||
|
||||
// position contains game-client coordinates
|
||||
bool client_position_valid = false;
|
||||
|
||||
// down began on the mode switch button; remains true through up
|
||||
bool mode_button = false;
|
||||
};
|
||||
|
||||
static std::atomic_bool accept_events { false };
|
||||
static std::atomic<Mode> current_mode_state { Mode::Nav };
|
||||
static std::mutex state_mutex;
|
||||
static TouchGeometry touch_geometry;
|
||||
|
||||
// native contacts are kept by ID so each contact contributes exactly one position
|
||||
static std::unordered_map<DWORD, NativeContact> active_contacts;
|
||||
|
||||
// a hardware button release requests one change after all contacts are released
|
||||
static bool mode_change_pending = false;
|
||||
|
||||
static void reset_state_locked() {
|
||||
current_mode_state.store(Mode::Nav, std::memory_order_release);
|
||||
touch_geometry = {};
|
||||
active_contacts.clear();
|
||||
mode_change_pending = false;
|
||||
}
|
||||
|
||||
// hardware contacts arrive in screen coordinates
|
||||
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
|
||||
if (!touch_geometry.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT position { event.x, event.y };
|
||||
if (!ScreenToClient(touch_geometry.window, &position)) {
|
||||
return false;
|
||||
}
|
||||
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
|
||||
// first, process down / move events
|
||||
if (event.down || event.move) {
|
||||
auto contact = active_contacts.try_emplace(event.id).first;
|
||||
|
||||
// keep track of IDs that began as a down on the mode switch button
|
||||
if (event.down) {
|
||||
contact->second.mode_button = native_touch_in_button(event);
|
||||
}
|
||||
|
||||
// check for valid position
|
||||
POINT position { event.x, event.y };
|
||||
if (touch_geometry.window != nullptr &&
|
||||
ScreenToClient(touch_geometry.window, &position)) {
|
||||
contact->second.position = position;
|
||||
contact->second.client_position_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto contact = active_contacts.find(event.id);
|
||||
const bool mode_button_contact = contact != active_contacts.end() &&
|
||||
contact->second.mode_button;
|
||||
|
||||
// process up events
|
||||
if (event.up) {
|
||||
active_contacts.erase(event.id);
|
||||
|
||||
// if a contact that began down event on the mode switch button has
|
||||
// been released, a mode switch is now pending
|
||||
if (mode_button_contact) {
|
||||
mode_change_pending = true;
|
||||
}
|
||||
|
||||
// apply the change on the final hardware up. switching earlier would
|
||||
// split another contact's down and up events across different modes
|
||||
if (mode_change_pending && active_contacts.empty()) {
|
||||
mode_change_pending = false;
|
||||
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
|
||||
current_mode_state.store(next_mode, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
return mode_button_contact;
|
||||
}
|
||||
|
||||
// install the Nostalgia-specific native touch interception
|
||||
void enable() {
|
||||
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(filter_native_touch);
|
||||
log_info("nost::touch", "enabled");
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(nullptr);
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
bool enabled() {
|
||||
return accept_events.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
Mode current_mode() {
|
||||
return current_mode_state.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// publish the rendered overlay button rectangle in game-client pixels
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds) {
|
||||
TouchGeometry next {};
|
||||
RECT client_rect {};
|
||||
if (window != nullptr && GetClientRect(window, &client_rect) &&
|
||||
client_rect.right > 0 && client_rect.bottom > 0) {
|
||||
next.window = window;
|
||||
next.mode_button = client_bounds;
|
||||
next.client_width = client_rect.right;
|
||||
next.client_height = client_rect.bottom;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
touch_geometry = next;
|
||||
}
|
||||
|
||||
// return the active 28-key piano bitfield for the PANB input update
|
||||
uint32_t piano_key_state() {
|
||||
if (!enabled() || current_mode() != Mode::Piano) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t state = 0;
|
||||
for (const auto &contact : active_contacts) {
|
||||
// invalid position or mode-button contact; ignore these contacts
|
||||
if (!contact.second.client_position_valid || contact.second.mode_button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &position = contact.second.position;
|
||||
|
||||
// outside the client area or on the mode button; ignore these contacts
|
||||
if (position.x < 0 || position.x >= touch_geometry.client_width ||
|
||||
position.y < 0 || position.y >= touch_geometry.client_height ||
|
||||
PtInRect(&touch_geometry.mode_button, position)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// divide the inset width evenly; touches in either side gap clamp to
|
||||
// the nearest outer key so the physical screen edges remain playable
|
||||
const auto piano_width =
|
||||
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
|
||||
uint32_t key = 0;
|
||||
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
|
||||
key = PIANO_KEY_COUNT - 1;
|
||||
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
|
||||
key = static_cast<uint32_t>(
|
||||
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
|
||||
}
|
||||
state |= UINT32_C(1) << key;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// update native contacts and report whether this event should be hidden from the game
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
|
||||
// synthetic events are outside this hardware-only feature
|
||||
if (!enabled() || event.synthetic) {
|
||||
// false leaves the event visible to the game
|
||||
return false;
|
||||
}
|
||||
|
||||
// snapshot routing before an up event can commit a pending mode switch
|
||||
const bool piano_mode_before_update = current_mode() == Mode::Piano;
|
||||
|
||||
// update the contact lifetime and commit any pending switch when safe
|
||||
const bool mode_button_contact = update_touch_state(event);
|
||||
|
||||
// hide every event in a contact that began on the mode switch button
|
||||
if (mode_button_contact) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// piano mode consumes hardware events; nav mode forwards them to the game
|
||||
return piano_mode_before_update;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// nav mode forwards contacts to the game; piano mode converts them into piano keys
|
||||
enum class Mode {
|
||||
Nav,
|
||||
Piano,
|
||||
};
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool enabled();
|
||||
|
||||
Mode current_mode();
|
||||
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds);
|
||||
|
||||
uint32_t piano_key_state();
|
||||
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "util/unity_player.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "io.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
@@ -19,13 +19,11 @@
|
||||
#include "util/sysutils.h"
|
||||
#include "io.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::popn {
|
||||
|
||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
@@ -715,15 +713,7 @@ namespace games::popn {
|
||||
// 00000100 0000000B 00000001 (button 9)
|
||||
// set third column to 0 and it will work with BIO2
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
|
||||
sysutils::hook_EnumDisplayDevicesA();
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ namespace games::popn {
|
||||
#endif
|
||||
|
||||
extern bool SHOW_PIKA_MONITOR_WARNING;
|
||||
extern bool NATIVE_TOUCH;
|
||||
|
||||
class POPNGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
||||
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
|
||||
}
|
||||
|
||||
// init stuff
|
||||
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
|
||||
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
log_info("rb", "not changing touch poll rate (120Hz by default)");
|
||||
log_info("rb", "not changing touch poll rate (~125Hz by default)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
namespace games::rb {
|
||||
|
||||
extern uint16_t TOUCH_SCALING;
|
||||
extern uint8_t TOUCH_SIZE;
|
||||
extern uint16_t TOUCH_POLL_RATE;
|
||||
|
||||
class RBGame : public games::Game {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch_debug.h"
|
||||
#include "touch_defs.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
@@ -12,28 +15,38 @@
|
||||
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
||||
|
||||
namespace games::rb {
|
||||
uint16_t TOUCH_SCALING = 1000;
|
||||
uint8_t TOUCH_SIZE = 1;
|
||||
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
|
||||
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
|
||||
|
||||
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
|
||||
|
||||
static void packet_set_bit(unsigned char *packet, int bit) {
|
||||
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
|
||||
static_cast<unsigned char>(1u << (bit % 8));
|
||||
}
|
||||
}
|
||||
|
||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
||||
this->log_fps = log_fps;
|
||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
|
||||
}
|
||||
|
||||
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
||||
|
||||
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
|
||||
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
|
||||
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
|
||||
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
|
||||
// read() contracts X uniformly so screen edges land within usable sensors 2..45
|
||||
int grid_x = CLAMP(
|
||||
(cursor_x * X_SENSOR_COUNT) / window_width,
|
||||
0,
|
||||
X_SENSOR_COUNT - 1);
|
||||
int grid_y = CLAMP(
|
||||
(cursor_y * Y_SENSOR_COUNT) / window_height,
|
||||
0,
|
||||
Y_SENSOR_COUNT - 1);
|
||||
|
||||
// get bit positions
|
||||
int bit_x = 88 + grid_x;
|
||||
int bit_y = 74 - grid_y;
|
||||
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
|
||||
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
|
||||
|
||||
// insert bits
|
||||
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
|
||||
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
|
||||
packet_set_bit(data, bit_x);
|
||||
packet_set_bit(data, bit_y);
|
||||
}
|
||||
|
||||
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
|
||||
if (wnd != nullptr) {
|
||||
|
||||
// cache the game window so read() can size against it regardless of focus
|
||||
this->game_hwnd = wnd;
|
||||
|
||||
// reset window process to make the game not crash
|
||||
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
||||
|
||||
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
// update window
|
||||
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} else {
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
// show game window because it lost focus
|
||||
// show fullscreen game window because it lost focus
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
ShowWindow(wnd, SW_SHOW);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// fallback to dx hook
|
||||
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
touch_debug_attach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||
|
||||
// check buffer size
|
||||
if (nNumberOfBytesToRead < 20) {
|
||||
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get window
|
||||
HWND window = GetForegroundWindow();
|
||||
// size against the game window cached in open() (not GetForegroundWindow), so
|
||||
// touch stays correctly scaled and keeps working when the game loses focus
|
||||
HWND window = this->game_hwnd;
|
||||
if (window == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// create data
|
||||
unsigned char data[20] {};
|
||||
unsigned char data[TOUCH_PACKET_SIZE] {};
|
||||
|
||||
// data header
|
||||
data[0] = 0x55;
|
||||
data[2] = 0x4C;
|
||||
|
||||
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
|
||||
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
|
||||
|
||||
// iterate all touch points
|
||||
auto offset_x = (int) (window_width / 48.0 / 3.0);
|
||||
auto offset_y = (int) (window_height / 76.0 / 3.0);
|
||||
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||
std::vector<TouchPoint> touch_points;
|
||||
touch_get_points(touch_points);
|
||||
for (auto &point : touch_points) {
|
||||
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// apply scaling
|
||||
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
|
||||
x = x / SCALE_FACTOR;
|
||||
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
|
||||
y = y / SCALE_FACTOR;
|
||||
x = x - (window_width * (1.f - scale_factor) / 2);
|
||||
x = x / scale_factor;
|
||||
y = y - (window_height * (1.f - scale_factor) / 2);
|
||||
y = y / scale_factor;
|
||||
|
||||
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// point scaling
|
||||
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
||||
const auto point_y = (int) (y - window_height / 76);
|
||||
// point coordinates
|
||||
// keep the verified left edge fixed while adding up to one-third of a beam
|
||||
// at the right edge, preventing the finger footprint from including sensor 44
|
||||
const auto point_x = (int) (
|
||||
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
|
||||
window_width / 2.0 +
|
||||
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
|
||||
const auto point_y = (int) y;
|
||||
|
||||
// center
|
||||
grid_insert(data, point_x, point_y);
|
||||
|
||||
// surrounding points
|
||||
if (games::rb::TOUCH_SIZE == 3) {
|
||||
grid_insert(data, point_x - offset_x, point_y); // west
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||
grid_insert(data, point_x + offset_x, point_y); // east
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
|
||||
grid_insert(data, point_x, point_y - offset_y); // north
|
||||
grid_insert(data, point_x, point_y + offset_y); // south
|
||||
}
|
||||
// model a finger as a 3x3 block of IR sensors around the touch point
|
||||
// this gives better accuracy (than just 1x1) since the logic below
|
||||
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
|
||||
// the center point, which means by inserting up to 8 extra blocks
|
||||
// we are emulating a sub-"pixel" resolution
|
||||
grid_insert(data, point_x, point_y); // center
|
||||
grid_insert(data, point_x - offset_x, point_y); // west
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||
grid_insert(data, point_x + offset_x, point_y); // east
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
|
||||
grid_insert(data, point_x, point_y - offset_y); // north
|
||||
grid_insert(data, point_x, point_y + offset_y); // south
|
||||
}
|
||||
|
||||
touch_debug_publish(data, is_landscape);
|
||||
|
||||
// copy data to buffer
|
||||
memcpy(lpBuffer, data, 20);
|
||||
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
|
||||
|
||||
// update frame logging
|
||||
if (log_fps) {
|
||||
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// return amount of bytes written
|
||||
return 20;
|
||||
return TOUCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
||||
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
|
||||
|
||||
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
||||
|
||||
touch_debug_detach();
|
||||
|
||||
// detach touch module
|
||||
touch_detach();
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace games::rb {
|
||||
int window_width = 1080;
|
||||
int window_height = 1920;
|
||||
|
||||
// game window resolved in open(); read() sizes against this instead of the
|
||||
// foreground window so touch stays correctly scaled and keeps working when
|
||||
// the game is not the foreground window (rawinput delivers touch regardless
|
||||
// of focus)
|
||||
HWND game_hwnd = nullptr;
|
||||
|
||||
// logging
|
||||
bool log_fps = false;
|
||||
uint64_t log_time = 0;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "touch_debug.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "games/rb/rb.h"
|
||||
#include "games/rb/touch_defs.h"
|
||||
|
||||
namespace games::rb {
|
||||
|
||||
struct TouchDebugState {
|
||||
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
|
||||
bool is_landscape = false;
|
||||
};
|
||||
|
||||
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
|
||||
static std::atomic_bool TOUCH_ACTIVE = false;
|
||||
static std::mutex TOUCH_DEBUG_STATE_M;
|
||||
static TouchDebugState TOUCH_DEBUG_STATE;
|
||||
|
||||
static float touch_scale_factor() {
|
||||
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
|
||||
}
|
||||
|
||||
static void clear_touch_debug_state() {
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
TOUCH_DEBUG_STATE = {};
|
||||
}
|
||||
|
||||
static TouchDebugState get_touch_debug_state() {
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
return TOUCH_DEBUG_STATE;
|
||||
}
|
||||
|
||||
static bool packet_bit_active(
|
||||
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
|
||||
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
|
||||
}
|
||||
|
||||
static int sensor_center(int sensor, int sensor_count, int extent) {
|
||||
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
|
||||
}
|
||||
|
||||
static float sensor_span_position(int sensor, int sensor_count, int extent) {
|
||||
return sensor * (extent - 1) / (float) (sensor_count - 1);
|
||||
}
|
||||
|
||||
bool touch_debug_overlay_enabled() {
|
||||
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void touch_draw_debug_overlay() {
|
||||
if (!touch_debug_overlay_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &io = ImGui::GetIO();
|
||||
int width = static_cast<int>(io.DisplaySize.x);
|
||||
int height = static_cast<int>(io.DisplaySize.y);
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float scale_factor = touch_scale_factor();
|
||||
const float left = width * (1.f - scale_factor) / 2.f;
|
||||
const float top = height * (1.f - scale_factor) / 2.f;
|
||||
const float right = width - left;
|
||||
const float bottom = height - top;
|
||||
|
||||
TouchDebugState state = get_touch_debug_state();
|
||||
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
|
||||
auto draw_line = [&](float x1, float y1, float x2, float y2) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(x1, y1), ImVec2(x2, y2),
|
||||
IM_COL32(0, 255, 64, 255), 2.f);
|
||||
};
|
||||
|
||||
// show the valid input area when touch scaling restricts it
|
||||
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
|
||||
draw_list->AddRect(
|
||||
ImVec2(left, top), ImVec2(right, bottom),
|
||||
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
|
||||
}
|
||||
|
||||
// spread the usable X sensors 2..45 from edge to edge
|
||||
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
|
||||
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float position = sensor_span_position(
|
||||
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
|
||||
state.is_landscape ? height : width);
|
||||
if (state.is_landscape) {
|
||||
float y = top + position * scale_factor;
|
||||
draw_line(left, y, right, y);
|
||||
} else {
|
||||
float x = left + position * scale_factor;
|
||||
draw_line(x, top, x, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
|
||||
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int position = sensor_center(
|
||||
sensor, Y_SENSOR_COUNT,
|
||||
state.is_landscape ? width : height);
|
||||
if (state.is_landscape) {
|
||||
float x = right - position * scale_factor;
|
||||
draw_line(x, top, x, bottom);
|
||||
} else {
|
||||
float y = top + position * scale_factor;
|
||||
draw_line(left, y, right, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void touch_debug_attach() {
|
||||
clear_touch_debug_state();
|
||||
TOUCH_ACTIVE.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void touch_debug_detach() {
|
||||
TOUCH_ACTIVE.store(false, std::memory_order_release);
|
||||
clear_touch_debug_state();
|
||||
}
|
||||
|
||||
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
|
||||
if (!TOUCH_DEBUG_OVERLAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
|
||||
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace games::rb {
|
||||
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
|
||||
|
||||
bool touch_debug_overlay_enabled();
|
||||
void touch_draw_debug_overlay();
|
||||
|
||||
void touch_debug_attach();
|
||||
void touch_debug_detach();
|
||||
void touch_debug_publish(const unsigned char *data, bool is_landscape);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::rb {
|
||||
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
|
||||
inline constexpr int TOUCH_PACKET_SIZE = 20;
|
||||
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
|
||||
|
||||
inline constexpr int X_SENSOR_COUNT = 48;
|
||||
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
|
||||
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
|
||||
inline constexpr int X_SENSOR_ACTIVE_COUNT =
|
||||
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
|
||||
inline constexpr int X_SENSOR_FIRST_BIT = 88;
|
||||
|
||||
inline constexpr int Y_SENSOR_COUNT = 76;
|
||||
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#ifdef SPICE64
|
||||
#include "hooks/graphics/nvapi_impl.h"
|
||||
#endif
|
||||
#include "hooks/graphics/nvapi_hook.h"
|
||||
#include "hooks/powrprof.h"
|
||||
#include "hooks/sleephook.h"
|
||||
@@ -23,8 +26,7 @@
|
||||
#include "util/libutils.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "camera.h"
|
||||
#include "io.h"
|
||||
@@ -46,7 +48,6 @@ namespace games::sdvx {
|
||||
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
||||
|
||||
// settings
|
||||
bool NATIVETOUCH = false;
|
||||
uint8_t DIGITAL_KNOB_SENS = 16;
|
||||
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
||||
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
||||
@@ -428,29 +429,31 @@ namespace games::sdvx {
|
||||
if (aio != nullptr) {
|
||||
|
||||
// enable 9on12 for AMD
|
||||
if (!libutils::try_library("nvapi64.dll")) {
|
||||
const auto nvapi = libutils::try_library("nvapi64.dll");
|
||||
if (nvapi == nullptr) {
|
||||
log_info(
|
||||
"sdvx",
|
||||
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
||||
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
|
||||
}
|
||||
|
||||
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
|
||||
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
|
||||
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
|
||||
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
|
||||
if (!nvapi_impl::initialize(
|
||||
avs::game::DLL_INSTANCE,
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz)) {
|
||||
log_warning("sdvx", "failed to initialize synthetic NVAPI");
|
||||
}
|
||||
} else {
|
||||
// don't let nvapi mess with display settings
|
||||
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
if (is_valkyrie_model()) {
|
||||
if (NATIVETOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
||||
// hook touch window
|
||||
// in windowed mode, game can accept mouse input on the second screen
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
"SOUND VOLTEX",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
|
||||
// insert BI2X hooks
|
||||
bi2x_hook_init();
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace games::sdvx {
|
||||
};
|
||||
|
||||
// settings
|
||||
extern bool NATIVETOUCH;
|
||||
extern uint8_t DIGITAL_KNOB_SENS;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern SdvxOverlayPosition OVERLAY_POS;
|
||||
|
||||
@@ -32,18 +32,28 @@ namespace hooks::audio {
|
||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||
index++) {
|
||||
|
||||
// read description (display name), fall back to the key name
|
||||
// read description (display name), fall back to the key name.
|
||||
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||
// because the latter is unavailable on Windows XP.
|
||||
char desc[256] = { 0 };
|
||||
DWORD size = sizeof(desc);
|
||||
std::string name = key_name;
|
||||
if (RegGetValueA(hkEnum,
|
||||
key_name,
|
||||
ASIO_REG_DESC,
|
||||
RRF_RT_REG_SZ | wow64_flag,
|
||||
nullptr,
|
||||
desc,
|
||||
&size) == ERROR_SUCCESS && desc[0]) {
|
||||
name = desc;
|
||||
HKEY hkDriver = nullptr;
|
||||
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||
DWORD type = 0;
|
||||
if (RegQueryValueExA(hkDriver,
|
||||
ASIO_REG_DESC,
|
||||
nullptr,
|
||||
&type,
|
||||
reinterpret_cast<LPBYTE>(desc),
|
||||
&size) == ERROR_SUCCESS
|
||||
&& type == REG_SZ && desc[0]) {
|
||||
// ensure null termination
|
||||
desc[sizeof(desc) - 1] = '\0';
|
||||
name = desc;
|
||||
}
|
||||
RegCloseKey(hkDriver);
|
||||
}
|
||||
|
||||
// merge with an existing entry from the other view (match by name)
|
||||
|
||||
@@ -154,36 +154,30 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
||||
// stale one down
|
||||
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
|
||||
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
|
||||
// is destroyed and re-created within a process - which the host triggers by leaking and
|
||||
// re-instantiating the driver during startup probing. so we build the real driver and
|
||||
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
|
||||
// the same wrapper back for every later CoCreate. as a result the real driver is created
|
||||
// and initialized exactly once
|
||||
std::mutex g_wrappers_mutex;
|
||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||
|
||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
||||
// replaces so the caller can tear it down outside the lock
|
||||
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||
WrappedAsio *find_wrapper(REFCLSID clsid) {
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
for (auto &entry : g_wrappers) {
|
||||
if (IsEqualCLSID(entry.first, clsid)) {
|
||||
WrappedAsio *stale = entry.second;
|
||||
entry.second = wrapper;
|
||||
return stale;
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
|
||||
g_wrappers.emplace_back(clsid, wrapper);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
||||
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
||||
if (it->second == wrapper) {
|
||||
g_wrappers.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_wrappers.emplace_back(clsid, wrapper);
|
||||
}
|
||||
|
||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
|
||||
}
|
||||
|
||||
WrappedAsio::~WrappedAsio() {
|
||||
unregister_wrapper(this);
|
||||
|
||||
this->detach_post_process();
|
||||
|
||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
||||
// exactly one here and this releases/unloads it deterministically
|
||||
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
|
||||
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
|
||||
// only once the host has released its own references. tears down the real driver
|
||||
this->pReal->Release();
|
||||
|
||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||
// the real driver is single-instance and kept alive; initialize it exactly once. the
|
||||
// host re-calls init() on each CoCreate during probing, but re-initializing a live
|
||||
// driver crashes some hardware drivers, so once we have a live driver we report success
|
||||
if (this->initialized) {
|
||||
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
|
||||
return AsioTrue;
|
||||
}
|
||||
|
||||
const AsioBool result = this->pReal->init(sys_handle);
|
||||
if (result == AsioTrue) {
|
||||
this->initialized = true;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"init succeeded for '{}' (driver version {})",
|
||||
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
|
||||
AsioError __thiscall WrappedAsio::start() {
|
||||
const AsioError result = this->pReal->start();
|
||||
if (result == ASE_OK) {
|
||||
this->started = true;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"start succeeded, ASIO stream is now running on '{}'",
|
||||
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
|
||||
AsioError __thiscall WrappedAsio::stop() {
|
||||
const AsioError result = this->pReal->stop();
|
||||
if (result == ASE_OK) {
|
||||
this->started = false;
|
||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
|
||||
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
||||
}
|
||||
|
||||
void WrappedAsio::quiesce_for_reuse() {
|
||||
// a previous abandoned probing cycle may have left a running stream and live buffers on
|
||||
// the real driver without calling stop/dispose; tear that down now (without destroying
|
||||
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
|
||||
// further buffer_switch, and dispose_buffers detaches our trampolines
|
||||
if (this->started) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"reuse: stopping leftover stream on '{}' before handing the driver back",
|
||||
this->driver_name);
|
||||
this->stop();
|
||||
}
|
||||
if (this->buffers_created) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"reuse: disposing leftover buffers on '{}' before handing the driver back",
|
||||
this->driver_name);
|
||||
this->dispose_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||
return;
|
||||
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
|
||||
}
|
||||
}
|
||||
this->publish_post_process(buffer_size);
|
||||
this->buffers_created = true;
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
|
||||
// device's 2.0 output. then publish ourselves to the realtime thread
|
||||
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
||||
this->publish_post_process(buffer_size);
|
||||
this->buffers_created = true;
|
||||
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
|
||||
this->volume_channels.clear();
|
||||
this->volume_active = false;
|
||||
this->downmix_active = false;
|
||||
this->buffers_created = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
|
||||
auto *wrapper = new WrappedAsio(
|
||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||
|
||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
||||
// deref once before re-initializing.
|
||||
//
|
||||
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
||||
// Xonar AE can
|
||||
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"host did not release previous instance for clsid={}, forcing teardown",
|
||||
guid2s(clsid));
|
||||
|
||||
// the stale instance may still have a running stream on the driver's realtime
|
||||
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
||||
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
||||
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
||||
// detaches our trampolines, fully quiescing the realtime path before delete
|
||||
stale->stop();
|
||||
stale->dispose_buffers();
|
||||
delete stale;
|
||||
}
|
||||
// pin the wrapper (and the real driver it owns) for the process lifetime; later
|
||||
// CoCreate calls reuse this same instance via wrap_existing
|
||||
wrapper->AddRef();
|
||||
store_wrapper(clsid, wrapper);
|
||||
|
||||
return static_cast<IAsio *>(wrapper);
|
||||
}
|
||||
|
||||
IUnknown *wrap_existing(REFCLSID clsid) {
|
||||
WrappedAsio *wrapper = find_wrapper(clsid);
|
||||
if (wrapper == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"audio::wrappedasio",
|
||||
"reusing cached ASIO driver instance, clsid={}",
|
||||
guid2s(clsid));
|
||||
|
||||
wrapper->quiesce_for_reuse();
|
||||
|
||||
// hand the host another reference to the one instance we keep alive
|
||||
wrapper->AddRef();
|
||||
return static_cast<IAsio *>(wrapper);
|
||||
}
|
||||
|
||||
void release_all_wrappers() {
|
||||
// collect the pinned wrappers under the lock, then drop our references outside it so
|
||||
// a final Release (which can run the driver's own teardown) never happens while the
|
||||
// lock is held
|
||||
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
|
||||
{
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
wrappers = g_wrappers;
|
||||
g_wrappers.clear();
|
||||
}
|
||||
|
||||
for (auto &entry : wrappers) {
|
||||
// drop only the pin wrap() took. if the host has already released its own
|
||||
// references (the normal case at shutdown) this destroys the wrapper and releases
|
||||
// the real driver; if the host is still using it, the refcount stays above zero
|
||||
// and we leave its live stream/buffers untouched
|
||||
log_misc(
|
||||
"audio::wrappedasio",
|
||||
"releasing cached ASIO driver instance, clsid={}",
|
||||
guid2s(entry.first));
|
||||
entry.second->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||
|
||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||
// return a proxy that forwards every call to it
|
||||
// return a proxy that forwards every call to it. also records it as the cached
|
||||
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||
|
||||
// if a cached wrapper already exists for this CLSID, return it (with an added
|
||||
// reference); otherwise nullptr to signal the caller to create the real driver and
|
||||
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
||||
IUnknown *wrap_existing(REFCLSID clsid);
|
||||
|
||||
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
||||
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
||||
// has released its own references too. call from a controlled shutdown point, never from
|
||||
// a static destructor (the driver DLL may already be unloaded)
|
||||
void release_all_wrappers();
|
||||
}
|
||||
|
||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
|
||||
AsioError __thiscall output_ready() override;
|
||||
#pragma endregion
|
||||
|
||||
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
||||
// for reuse, without destroying the real driver (see wrap_existing)
|
||||
void quiesce_for_reuse();
|
||||
|
||||
private:
|
||||
// create_buffers implementation used when a stereo extraction is active: forwards only
|
||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||
@@ -170,6 +186,14 @@ private:
|
||||
// unambiguous name; constant for our lifetime
|
||||
std::string driver_name;
|
||||
|
||||
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
||||
bool initialized = false;
|
||||
|
||||
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
||||
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
||||
bool buffers_created = false;
|
||||
bool started = false;
|
||||
|
||||
// our own reference count; we hold one reference on pReal and release it when this
|
||||
// drops to zero
|
||||
std::atomic<ULONG> ref_count {1};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "audio_private.h"
|
||||
#include "acm.h"
|
||||
#include "asio_proxy.h"
|
||||
#include "xact.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
@@ -53,9 +55,39 @@ namespace hooks::audio {
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
// private globals
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
std::mutex INITIALIZE_LOCK; // for asio
|
||||
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
static std::mutex CLIENT_LOCK;
|
||||
static bool CLIENT_STOPPING = false;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source) {
|
||||
IAudioClient *previous_client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
|
||||
if (CLIENT_STOPPING || CLIENT == client) {
|
||||
return;
|
||||
}
|
||||
if (client) {
|
||||
client->AddRef();
|
||||
}
|
||||
|
||||
previous_client = CLIENT;
|
||||
CLIENT = client;
|
||||
}
|
||||
|
||||
if (previous_client) {
|
||||
previous_client->Release();
|
||||
}
|
||||
|
||||
if (client) {
|
||||
log_info(
|
||||
"audio",
|
||||
"active client selected by {} after successful initialization",
|
||||
source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
@@ -68,6 +100,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
|
||||
HRESULT ret;
|
||||
|
||||
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
|
||||
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
|
||||
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
|
||||
*ppv = reused;
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// call original
|
||||
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||
if (FAILED(ret)) {
|
||||
@@ -120,15 +161,28 @@ namespace hooks::audio {
|
||||
|
||||
// general hooks
|
||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||
if (avs::game::is_model("PAN")) {
|
||||
hooks::audio::xact::init();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
log_info("audio", "stopping");
|
||||
if (CLIENT) {
|
||||
CLIENT->Stop();
|
||||
CLIENT->Release();
|
||||
IAudioClient *client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
CLIENT_STOPPING = true;
|
||||
client = CLIENT;
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
if (client) {
|
||||
client->Stop();
|
||||
client->Release();
|
||||
}
|
||||
stop_low_latency();
|
||||
|
||||
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||
// at a controlled shutdown point (see asio_proxy.cpp)
|
||||
hooks::audio::asio::release_all_wrappers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ namespace hooks::audio {
|
||||
extern std::mutex INITIALIZE_LOCK;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source);
|
||||
}
|
||||
|
||||
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
}
|
||||
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
||||
|
||||
// release old audio client if initialized
|
||||
if (hooks::audio::CLIENT) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
IAudioClient *client = nullptr;
|
||||
if (iid == IID_IAudioClient) {
|
||||
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
||||
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
||||
}
|
||||
*ppInterface = client;
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||
|
||||
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// release any previously persisted client
|
||||
if (hooks::audio::CLIENT != nullptr) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||
}
|
||||
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
@@ -651,5 +652,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
HRESULT ret = this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
AudioSessionGuid);
|
||||
if (SUCCEEDED(ret)) {
|
||||
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
|
||||
}
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
#include "xact.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <mmreg.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
|
||||
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
|
||||
// layout, so keep this proxy pinned to the legacy ABI used by libxact.
|
||||
struct XAudio2DeviceDetails {
|
||||
WCHAR device_id[256];
|
||||
WCHAR display_name[256];
|
||||
DWORD role;
|
||||
WAVEFORMATEXTENSIBLE output_format;
|
||||
};
|
||||
|
||||
struct XAudio2EffectChain {
|
||||
UINT32 effect_count;
|
||||
const void *effect_descriptors;
|
||||
};
|
||||
|
||||
struct IXAudio2_27 {
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE Release() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
|
||||
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
|
||||
virtual void STDMETHODCALLTYPE StopEngine() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
|
||||
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
|
||||
virtual void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) = 0;
|
||||
};
|
||||
|
||||
// XAudio2 2.7 COM class and interface.
|
||||
DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
|
||||
0x5a508685, 0xa254, 0x4fba,
|
||||
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
|
||||
DEFINE_GUID(IID_IXAudio2_7_LEGACY,
|
||||
0x8bcf1f58, 0x9fe7, 0x4583,
|
||||
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
|
||||
|
||||
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
|
||||
|
||||
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
|
||||
static CreateFX_t CreateFX_orig = nullptr;
|
||||
|
||||
static std::string describe_wave_format(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
DWORD channel_mask = 0;
|
||||
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
|
||||
}
|
||||
|
||||
return fmt::format(
|
||||
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
|
||||
format->wFormatTag,
|
||||
format->nChannels,
|
||||
format->nSamplesPerSec,
|
||||
format->wBitsPerSample,
|
||||
format->nBlockAlign,
|
||||
format->nAvgBytesPerSec,
|
||||
channel_mask);
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
static std::string narrow_fixed(const WCHAR (&value)[Size]) {
|
||||
size_t length = 0;
|
||||
while (length < Size && value[length] != L'\0') {
|
||||
length++;
|
||||
}
|
||||
return ws2s(std::wstring(value, length));
|
||||
}
|
||||
|
||||
class WrappedXAudio2 final : public IXAudio2_27 {
|
||||
public:
|
||||
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
|
||||
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
|
||||
if (object == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = this;
|
||||
AddRef();
|
||||
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const auto result = real->QueryInterface(riid, object);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::QueryInterface({}) -> {}, object={}",
|
||||
guid2s(riid),
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++ref_count;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const auto remaining = --ref_count;
|
||||
if (remaining == 0) {
|
||||
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
|
||||
real->Release();
|
||||
delete this;
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
|
||||
const auto result = real->GetDeviceCount(device_count);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceCount -> {}, count={}",
|
||||
FMT_HRESULT(result),
|
||||
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) override {
|
||||
const auto result = real->GetDeviceDetails(device_index, device_details);
|
||||
if (SUCCEEDED(result) && device_details != nullptr) {
|
||||
const auto device_name = narrow_fixed(device_details->display_name);
|
||||
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result),
|
||||
narrow_fixed(device_details->device_id),
|
||||
device_name,
|
||||
device_details->role,
|
||||
describe_wave_format(&device_details->output_format.Format));
|
||||
}
|
||||
const auto channels = device_details->output_format.Format.nChannels;
|
||||
if (channels != 2 && channels != 6 &&
|
||||
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
|
||||
device_name,
|
||||
channels);
|
||||
deferredlogs::defer_error_messages({
|
||||
"unsupported audio output channel count detected!",
|
||||
fmt::format(" device: {}", device_name),
|
||||
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
|
||||
" * configure the default Windows playback device for stereo or 5.1 output",
|
||||
" * disable 7.1 surround sound or spatial audio for this device",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
|
||||
const auto result = real->Initialize(flags, processor);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
|
||||
flags,
|
||||
processor,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
|
||||
const auto result = real->RegisterForCallbacks(callback);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::RegisterForCallbacks({}) -> {}",
|
||||
callback,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
|
||||
real->UnregisterForCallbacks(callback);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSourceVoice(
|
||||
source_voice,
|
||||
source_format,
|
||||
flags,
|
||||
max_frequency_ratio,
|
||||
callback,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSubmixVoice(
|
||||
submix_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
processing_stage,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
const auto result = real->CreateMasteringVoice(
|
||||
mastering_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain != nullptr ? effect_chain->effect_count : 0,
|
||||
FMT_HRESULT(result),
|
||||
mastering_voice != nullptr ? *mastering_voice : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE StartEngine() override {
|
||||
const auto result = real->StartEngine();
|
||||
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE StopEngine() override {
|
||||
log_info("audio::xaudio2", "IXAudio2::StopEngine");
|
||||
real->StopEngine();
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
|
||||
return real->CommitChanges(operation_set);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
|
||||
real->GetPerformanceData(performance_data);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
|
||||
real->SetDebugConfiguration(debug_configuration, reserved);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_count = 1;
|
||||
std::atomic_bool device_details_logged = false;
|
||||
std::atomic_bool channel_warning_logged = false;
|
||||
IXAudio2_27 *real;
|
||||
};
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
REFCLSID clsid,
|
||||
LPUNKNOWN outer,
|
||||
DWORD class_context,
|
||||
REFIID iid,
|
||||
LPVOID *object) {
|
||||
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
|
||||
guid2s(clsid),
|
||||
guid2s(iid),
|
||||
class_context,
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
|
||||
if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
|
||||
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
|
||||
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = static_cast<IXAudio2_27 *>(
|
||||
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static HRESULT WINAPI CreateFX_hook(
|
||||
REFCLSID clsid,
|
||||
IUnknown **effect,
|
||||
const void *init_data,
|
||||
UINT32 init_data_size) {
|
||||
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
|
||||
log_info(
|
||||
"audio::xapofx",
|
||||
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
|
||||
guid2s(clsid),
|
||||
init_data,
|
||||
init_data_size,
|
||||
FMT_HRESULT(result),
|
||||
effect != nullptr ? static_cast<void *>(*effect) : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
void init() {
|
||||
const auto libxact = GetModuleHandleW(L"libxact.dll");
|
||||
if (libxact == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoCreateInstance_orig = detour::iat_try(
|
||||
"CoCreateInstance", CoCreateInstance_hook, libxact);
|
||||
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
|
||||
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"libxact hooks installed: CoCreateInstance={}, CreateFX={}",
|
||||
CoCreateInstance_orig != nullptr,
|
||||
CreateFX_orig != nullptr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
void init();
|
||||
}
|
||||
@@ -23,12 +23,12 @@
|
||||
#include "launcher/shutdown.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/touch_indicators.h"
|
||||
#include "touch/touch_gestures.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/utils.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "util/time.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
|
||||
@@ -188,11 +188,13 @@ static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
||||
}
|
||||
|
||||
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_override(window_name);
|
||||
}
|
||||
|
||||
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
||||
if (window_name == "LEFT") {
|
||||
if (window_name == "GITADORA") {
|
||||
GRAPHICS_HOOKED_WINDOW = hWnd;
|
||||
} else if (window_name == "LEFT") {
|
||||
GFDM_LEFT_WINDOW = hWnd;
|
||||
} else if (window_name == "RIGHT") {
|
||||
GFDM_RIGHT_WINDOW = hWnd;
|
||||
@@ -305,22 +307,6 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
|
||||
}
|
||||
}
|
||||
|
||||
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
|
||||
// drop mouse inputs since only wintouches should be used
|
||||
switch (uMsg) {
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_LBUTTONUP:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_XBUTTONDOWN:
|
||||
case WM_XBUTTONUP:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// window resize
|
||||
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
|
||||
|
||||
@@ -589,6 +575,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
hWndParent, hMenu, hInstance, lpParam);
|
||||
GRAPHICS_WINDOWS.push_back(result);
|
||||
|
||||
// remember these windows now because full capture happens later during D3D
|
||||
// initialization; placement calls before then can undo creation-time overrides
|
||||
if (result != nullptr && is_gfdm_window && !is_gfdm_sub_window) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
|
||||
// theme the native title bar (dark/light)
|
||||
set_window_dark_titlebar(result);
|
||||
|
||||
@@ -609,9 +601,6 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
}
|
||||
|
||||
// only hook touch window if multiple windows are allowed
|
||||
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||
@@ -627,7 +616,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
}
|
||||
}
|
||||
|
||||
disable_touch_indicators(result);
|
||||
disable_touch_gestures(result);
|
||||
log_misc(
|
||||
"graphics",
|
||||
"CreateWindowExA returned {}, {}",
|
||||
@@ -722,7 +711,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
|
||||
fmt::ptr(result),
|
||||
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
||||
|
||||
disable_touch_indicators(result);
|
||||
disable_touch_gestures(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -797,7 +786,7 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
||||
nWidth = rect.right - rect.left;
|
||||
nHeight = rect.bottom - rect.top;
|
||||
|
||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||
} else {
|
||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||
log_info(
|
||||
@@ -950,6 +939,13 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (games::iidx::TDJ_MODE &&
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||
hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||
return true;
|
||||
}
|
||||
|
||||
// call original
|
||||
return ShowWindow_orig(hWnd, nCmdShow);
|
||||
}
|
||||
@@ -1163,6 +1159,13 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
||||
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||
|
||||
const bool native_touch_overlay =
|
||||
(games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||
(games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||
if (native_touch_overlay) {
|
||||
nativetouch::inject::register_and_attach_window(hWnd);
|
||||
}
|
||||
|
||||
// NOLEGACY causes WM_CHAR to be not received
|
||||
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
||||
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
||||
|
||||
@@ -88,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
extern bool GRAPHICS_IIDX_WSUB;
|
||||
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
|
||||
@@ -157,7 +159,7 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change);
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name);
|
||||
|
||||
void change_primary_monitor(const std::string &monitor_name);
|
||||
void update_monitor_on_boot(
|
||||
|
||||
@@ -30,6 +30,8 @@ std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
|
||||
bool GRAPHICS_IIDX_WSUB = false;
|
||||
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
|
||||
return empty;
|
||||
}
|
||||
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name) {
|
||||
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
||||
return !monitor_name.empty();
|
||||
return !monitor_name.empty() ||
|
||||
(window_name == "SMALL" &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() ||
|
||||
GRAPHICS_GITADORA_SMALL_POS.has_value()));
|
||||
}
|
||||
|
||||
static bool graphics_monitor_rect_from_name(
|
||||
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change) {
|
||||
return graphics_gitadora_apply_monitor_rect(
|
||||
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applied = graphics_gitadora_apply_monitor_rect(
|
||||
graphics_gitadora_monitor_for_window_name(window_name),
|
||||
window_name,
|
||||
x,
|
||||
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
width,
|
||||
height,
|
||||
log_change);
|
||||
|
||||
if (window_name != "SMALL") {
|
||||
return applied;
|
||||
}
|
||||
|
||||
std::pair<uint32_t, uint32_t> result;
|
||||
if (GRAPHICS_GITADORA_SMALL_SIZE.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_SIZE.value(), result)) {
|
||||
width = result.first;
|
||||
height = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallsize");
|
||||
}
|
||||
}
|
||||
if (GRAPHICS_GITADORA_SMALL_POS.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_POS.value(), result)) {
|
||||
x = result.first;
|
||||
y = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallpos");
|
||||
}
|
||||
}
|
||||
|
||||
if (applied && log_change &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() || GRAPHICS_GITADORA_SMALL_POS.has_value())) {
|
||||
log_info(
|
||||
"graphics-windowed",
|
||||
"GITADORA SMALL custom override: pos=({}, {}), size={}x{}",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
void graphics_capture_initial_window(HWND hWnd) {
|
||||
@@ -254,7 +299,7 @@ void graphics_capture_initial_window(HWND hWnd) {
|
||||
cfg::SCREENRESIZE->init_client_width = client_w;
|
||||
cfg::SCREENRESIZE->init_client_height = client_h;
|
||||
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
|
||||
log_debug(
|
||||
log_misc(
|
||||
"graphics-windowed",
|
||||
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
|
||||
fmt::ptr(hWnd),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "external/nvapi/NvApiDriverSettings.h"
|
||||
#include "avs/game.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
@@ -12,14 +13,21 @@ namespace nvapi_hook {
|
||||
bool BYPASS_NVAPI = false;
|
||||
|
||||
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
|
||||
|
||||
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
|
||||
static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr;
|
||||
static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr;
|
||||
|
||||
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags);
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
|
||||
|
||||
void initialize(HINSTANCE dll) {
|
||||
|
||||
#ifdef SPICE64
|
||||
std::string nvapi_dll = "nvapi64.dll";
|
||||
#else
|
||||
@@ -40,6 +48,18 @@ namespace nvapi_hook {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// spoof display connector types only for SDVX
|
||||
if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) {
|
||||
NvAPI_GPU_GetConnectedDisplayIds_orig =
|
||||
reinterpret_cast<decltype(NvAPI_GPU_GetConnectedDisplayIds) *>(
|
||||
NvAPI_QueryInterface_orig(func_code));
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig =
|
||||
reinterpret_cast<decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *>(
|
||||
NvAPI_QueryInterface_orig(0x1E9D8A31));
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked");
|
||||
return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook;
|
||||
}
|
||||
|
||||
// NvAPI_DISP_SetDisplayConfig
|
||||
if (func_code == 0x5D8CF8DE) {
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
|
||||
@@ -51,8 +71,53 @@ namespace nvapi_hook {
|
||||
return NvAPI_QueryInterface_orig(func_code);
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags) {
|
||||
|
||||
if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) {
|
||||
return NVAPI_NO_IMPLEMENTATION;
|
||||
}
|
||||
|
||||
const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig(
|
||||
hPhysicalGpu,
|
||||
pDisplayIds,
|
||||
pDisplayIdCount,
|
||||
flags);
|
||||
|
||||
if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr ||
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) {
|
||||
return status;
|
||||
}
|
||||
|
||||
NvU32 primary_display_id = 0;
|
||||
if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// report the primary display adapter as DP and all other displays as HDMI
|
||||
// (the game expects this since the VM cab is like that)
|
||||
//
|
||||
// this doesn't actually affect gameplay; only how
|
||||
// NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz
|
||||
// gets printed.
|
||||
//
|
||||
// for the actual value that matters for gameplay, it's this:
|
||||
// NvDisplayConfig: GetMainDisplayRefreshRate = {}hz
|
||||
for (NvU32 index = 0; index < *pDisplayIdCount; index++) {
|
||||
auto &display = pDisplayIds[index];
|
||||
display.connectorType = display.displayId == primary_display_id ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
|
||||
// do not let the game apply its emulated cabinet topology to Windows
|
||||
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
#include "nvapi_impl.h"
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sysutils.h"
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
|
||||
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
|
||||
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
|
||||
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
|
||||
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
|
||||
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
|
||||
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
|
||||
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
|
||||
|
||||
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
|
||||
|
||||
struct SyntheticDisplay {
|
||||
NvU32 display_id;
|
||||
NvU32 width;
|
||||
NvU32 height;
|
||||
NvU32 color_depth;
|
||||
NvS32 x;
|
||||
NvS32 y;
|
||||
NvU32 refresh_rate_1k;
|
||||
NV_ROTATE rotation;
|
||||
bool primary;
|
||||
};
|
||||
|
||||
static bool provider_initialized = false;
|
||||
static bool nvapi_initialized = false;
|
||||
static int gpu_handle_storage = 0;
|
||||
// snapshot of the Win32 display state exposed through synthetic NVAPI
|
||||
static std::vector<SyntheticDisplay> displays;
|
||||
|
||||
static NvPhysicalGpuHandle get_gpu_handle() {
|
||||
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
|
||||
}
|
||||
|
||||
static NV_ROTATE get_rotation(DWORD orientation) {
|
||||
switch (orientation) {
|
||||
case DMDO_90:
|
||||
return NV_ROTATE_90;
|
||||
case DMDO_180:
|
||||
return NV_ROTATE_180;
|
||||
case DMDO_270:
|
||||
return NV_ROTATE_270;
|
||||
default:
|
||||
return NV_ROTATE_0;
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<SyntheticDisplay> enumerate_displays(
|
||||
uint32_t main_refresh_hz,
|
||||
uint32_t sub_refresh_hz) {
|
||||
|
||||
std::vector<SyntheticDisplay> result;
|
||||
|
||||
// reuse the active monitor list, then read live modes after -mainmonitor changes
|
||||
for (const auto &monitor : sysutils::enumerate_monitors()) {
|
||||
DEVMODEA mode {};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (!EnumDisplaySettingsExA(
|
||||
monitor.display_name.c_str(),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mode,
|
||||
0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = mode.dmPelsWidth,
|
||||
.height = mode.dmPelsHeight,
|
||||
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
|
||||
.x = mode.dmPosition.x,
|
||||
.y = mode.dmPosition.y,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = get_rotation(mode.dmDisplayOrientation),
|
||||
.primary = primary,
|
||||
});
|
||||
}
|
||||
|
||||
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
|
||||
return left.primary && !right.primary;
|
||||
});
|
||||
|
||||
if (result.size() > 2) {
|
||||
result.resize(2);
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
.color_depth = 32,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = NV_ROTATE_0,
|
||||
.primary = true,
|
||||
});
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < result.size(); index++) {
|
||||
auto &display = result[index];
|
||||
display.primary = index == 0;
|
||||
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
|
||||
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
|
||||
display.refresh_rate_1k = refresh_hz * 1000;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// initializes NVAPI for the calling process.
|
||||
// marks the synthetic provider initialized without contacting a driver.
|
||||
static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Initialize");
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// initializes NVAPI with additional client flags.
|
||||
// accepts the flags and marks the synthetic provider initialized.
|
||||
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
|
||||
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// releases NVAPI state held for the calling process.
|
||||
// clears the synthetic initialization state while leaving the provider installed.
|
||||
static NvAPI_Status __cdecl NvAPI_Unload_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Unload");
|
||||
nvapi_initialized = false;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// enumerates physical GPU handles managed by the NVIDIA driver.
|
||||
// returns one stable synthetic GPU containing all exposed displays.
|
||||
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
|
||||
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
|
||||
NvU32 *gpu_count) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs(handles={}, count={})",
|
||||
fmt::ptr(gpu_handles),
|
||||
fmt::ptr(gpu_count));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handles == nullptr || gpu_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
gpu_handles[0] = get_gpu_handle();
|
||||
*gpu_count = 1;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
|
||||
fmt::ptr(gpu_handles[0]),
|
||||
*gpu_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns connected display descriptors for a physical GPU.
|
||||
// exposes the monitor snapshot as DP primary and HDMI secondary displays.
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
|
||||
NvPhysicalGpuHandle gpu_handle,
|
||||
NV_GPU_DISPLAYIDS *display_ids,
|
||||
NvU32 *display_id_count,
|
||||
NvU32 flags) {
|
||||
|
||||
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
|
||||
fmt::ptr(gpu_handle),
|
||||
fmt::ptr(display_ids),
|
||||
input_count,
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handle != get_gpu_handle()) {
|
||||
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
|
||||
}
|
||||
if (display_id_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (display_ids == nullptr) {
|
||||
*display_id_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *display_id_count;
|
||||
*display_id_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
const auto &source = displays[index];
|
||||
auto &destination = display_ids[index];
|
||||
destination = {};
|
||||
destination.version = NV_GPU_DISPLAYIDS_VER;
|
||||
destination.connectorType = source.primary ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
destination.displayId = source.display_id;
|
||||
destination.isActive = 1;
|
||||
destination.isOSVisible = 1;
|
||||
destination.isConnected = 1;
|
||||
destination.isPhysicallyConnected = 1;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns the NVAPI display ID associated with the Windows GDI primary.
|
||||
// returns the first synthetic display, ordered from the live desktop origin.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
|
||||
fmt::ptr(display_id));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (display_id == nullptr || displays.empty()) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
*display_id = displays.front().display_id;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
|
||||
*display_id);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
static void fill_source_mode(
|
||||
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
|
||||
const SyntheticDisplay &source) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
*destination = {};
|
||||
destination->resolution.width = source.width;
|
||||
destination->resolution.height = source.height;
|
||||
destination->resolution.colorDepth = source.color_depth;
|
||||
destination->colorFormat = NV_FORMAT_A8R8G8B8;
|
||||
destination->position.x = source.x;
|
||||
destination->position.y = source.y;
|
||||
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
|
||||
destination->bGDIPrimary = source.primary ? 1 : 0;
|
||||
}
|
||||
|
||||
static NvAPI_Status fill_target(
|
||||
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
|
||||
const SyntheticDisplay &source,
|
||||
NvU32 target_id) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
auto *details = destination->details;
|
||||
destination->displayId = source.display_id;
|
||||
destination->targetId = target_id;
|
||||
|
||||
if (details == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
|
||||
*details = {};
|
||||
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
|
||||
details->rotation = source.rotation;
|
||||
details->scaling = NV_SCALING_DEFAULT;
|
||||
details->refreshRate1K = source.refresh_rate_1k;
|
||||
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// retrieves the current global display topology through NVAPI's three-pass contract.
|
||||
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
|
||||
NvU32 *path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info) {
|
||||
|
||||
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
|
||||
input_count,
|
||||
fmt::ptr(path_info));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (path_info_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (path_info == nullptr) {
|
||||
*path_info_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *path_info_count;
|
||||
*path_info_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
auto &path = path_info[index];
|
||||
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
const auto &display = displays[index];
|
||||
path.sourceId = index;
|
||||
path.targetInfoCount = 1;
|
||||
path.IsNonNVIDIAAdapter = 0;
|
||||
path.pOSAdapterID = nullptr;
|
||||
fill_source_mode(path.sourceModeInfo, display);
|
||||
|
||||
const NvAPI_Status status = fill_target(path.targetInfo, display, index);
|
||||
if (status != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// applies a supplied global display topology through the NVIDIA driver.
|
||||
// accepts the cabinet topology without making any changes to Windows.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
|
||||
NvU32 path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info,
|
||||
NvU32 flags) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
|
||||
path_info_count,
|
||||
fmt::ptr(path_info),
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static uintptr_t *query_result(T function) {
|
||||
return reinterpret_cast<uintptr_t *>(function);
|
||||
}
|
||||
|
||||
// resolves an NVAPI function ID to its implementation address.
|
||||
// exposes only the synthetic entry points used by KFC and rejects all others.
|
||||
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
|
||||
uintptr_t *result = nullptr;
|
||||
switch (function_id) {
|
||||
case NVAPI_INITIALIZE_ID:
|
||||
result = query_result(NvAPI_Initialize_impl);
|
||||
break;
|
||||
case NVAPI_INITIALIZE_EX_ID:
|
||||
result = query_result(NvAPI_InitializeEx_impl);
|
||||
break;
|
||||
case NVAPI_UNLOAD_ID:
|
||||
result = query_result(NvAPI_Unload_impl);
|
||||
break;
|
||||
case NVAPI_ENUM_PHYSICAL_GPUS_ID:
|
||||
result = query_result(NvAPI_EnumPhysicalGPUs_impl);
|
||||
break;
|
||||
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
|
||||
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
|
||||
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
|
||||
break;
|
||||
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_QueryInterface(0x{:x}) - {}",
|
||||
function_id,
|
||||
result != nullptr ? "implemented" : "unsupported");
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
|
||||
if (provider_initialized) {
|
||||
return true;
|
||||
}
|
||||
if (dll == nullptr) {
|
||||
log_warning("nvapi_impl", "invalid synthetic module handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
|
||||
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
|
||||
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
|
||||
libraryhook_enable();
|
||||
|
||||
provider_initialized = true;
|
||||
log_info(
|
||||
"nvapi_impl",
|
||||
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
|
||||
NVAPI_DLL_NAME_A,
|
||||
displays.size(),
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,7 @@ static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
||||
|
||||
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
|
||||
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
|
||||
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
|
||||
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
|
||||
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
|
||||
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
|
||||
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
|
||||
return LoadLibraryW_orig(lpFileName);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
|
||||
|
||||
// check hooks
|
||||
if (lpFileName) {
|
||||
auto module = LIBRARIES_W.find(lpFileName);
|
||||
if (module != LIBRARIES_W.end()) {
|
||||
return module->second;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
||||
|
||||
// check hooks
|
||||
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
|
||||
// detour
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/qma/qma.h"
|
||||
#include "games/rb/rb.h"
|
||||
#include "games/rb/touch_debug.h"
|
||||
#include "games/rf3d/rf3d.h"
|
||||
#include "games/sc/sc.h"
|
||||
#include "games/scotto/scotto.h"
|
||||
@@ -101,7 +102,7 @@
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "overlay/windows/patch_manager.h"
|
||||
#include "patcher/patch_manager.h"
|
||||
#include "overlay/windows/iidx_seg.h"
|
||||
#include "overlay/windows/obs.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
@@ -378,6 +379,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
@@ -427,9 +429,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
||||
D3D9_DEVICE_HOOK_DISABLE = true;
|
||||
// touch emulation gets disabled, might as well turn these on
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
|
||||
overlay::ENABLED = false;
|
||||
// leaving these on without dx9hooks result in torn state and therefore failure to draw
|
||||
@@ -489,9 +488,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
|
||||
attach_sdvx = true;
|
||||
}
|
||||
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
|
||||
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
|
||||
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
|
||||
@@ -592,9 +588,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
|
||||
games::iidx::DISABLE_ESPEC_IO = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNativeTouch].value_bool()) {
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
}
|
||||
// should come later since this will override a few settings
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
|
||||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
|
||||
@@ -642,9 +635,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
|
||||
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
||||
games::popn::NATIVE_TOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||
SUBSCREEN_FORCE_REDRAW = true;
|
||||
}
|
||||
@@ -888,7 +878,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
||||
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
options[launcher::Options::PatchManagerConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
||||
@@ -1064,6 +1054,15 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||
rawinput::touch::INVERTED = true;
|
||||
}
|
||||
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
|
||||
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
|
||||
if (mode == "on") {
|
||||
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
|
||||
} else if (mode == "off") {
|
||||
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
|
||||
}
|
||||
// "auto" leaves the default (per-game)
|
||||
}
|
||||
// DisableTouchCardInsert is no longer honored in spice2x
|
||||
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
||||
// SPICETOUCH_CARD_DISABLE = true;
|
||||
@@ -1105,6 +1104,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
||||
overlay::ENABLED = false;
|
||||
}
|
||||
if (options[launcher::Options::OverlayKeyboardNavigation].value_bool()) {
|
||||
overlay::ENABLE_KEYBOARD_NAVIGATION = true;
|
||||
}
|
||||
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
||||
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
||||
if (10 <= val && val <= 400 && val != 100) {
|
||||
@@ -1273,6 +1275,14 @@ int main_implementation(int argc, char *argv[]) {
|
||||
GRAPHICS_GITADORA_SMALL_MONITOR =
|
||||
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallSize].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_SIZE =
|
||||
options[launcher::Options::GitaDoraWindowedSmallSize].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallPosition].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_POS =
|
||||
options[launcher::Options::GitaDoraWindowedSmallPosition].value_text();
|
||||
}
|
||||
|
||||
// IIDX/SDVX Windowed Subscreen
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
|
||||
@@ -1305,21 +1315,36 @@ int main_implementation(int argc, char *argv[]) {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
|
||||
} else {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||
}
|
||||
}
|
||||
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
|
||||
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
|
||||
if (mode == "none") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
|
||||
} else if (mode == "box") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
|
||||
} else if (mode == "all") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
|
||||
} else {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
|
||||
}
|
||||
}
|
||||
|
||||
if (options[launcher::Options::JubeatTouchDebounce].is_active()) {
|
||||
games::jb::TOUCH_DEBOUNCE_MS =
|
||||
options[launcher::Options::JubeatTouchDebounce].value_uint32();
|
||||
}
|
||||
|
||||
// reflec beat touch emulation
|
||||
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
|
||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
||||
}
|
||||
if (options[launcher::Options::RBTouchSize].is_active()) {
|
||||
const auto text = options[launcher::Options::RBTouchSize].value_text();
|
||||
if (text == "3") {
|
||||
games::rb::TOUCH_SIZE = 3;
|
||||
}
|
||||
}
|
||||
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
|
||||
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
||||
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
||||
}
|
||||
@@ -1761,7 +1786,16 @@ int main_implementation(int argc, char *argv[]) {
|
||||
games::iidx::poke::enable();
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
||||
games::nost::ENABLE_POKE = TRUE;
|
||||
games::nost::ENABLE_POKE = true;
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaTouchMode].is_active()) {
|
||||
if (overlay::ENABLED) {
|
||||
games::nost::ENABLE_TOUCH_MODE = true;
|
||||
} else {
|
||||
log_warning(
|
||||
"launcher",
|
||||
"Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2489,10 +2523,8 @@ int main_implementation(int argc, char *argv[]) {
|
||||
// copy defaults to nvram
|
||||
avs::core::copy_defaults();
|
||||
|
||||
// prepare patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, false);
|
||||
}
|
||||
// prepare patches (registers the DLL-load notification before the game DLL loads)
|
||||
patcher::init();
|
||||
|
||||
// load game
|
||||
avs::game::load_dll();
|
||||
@@ -2620,9 +2652,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
// apply patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, true);
|
||||
}
|
||||
patcher::apply_patches_on_start();
|
||||
|
||||
// load AVS-EA3
|
||||
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
||||
|
||||
@@ -434,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Advanced Network",
|
||||
},
|
||||
{
|
||||
.title = "Preferred NetAdapter IP",
|
||||
.title = "Preferred Network Adapter's IP",
|
||||
.name = "network",
|
||||
.desc = "This is NOT the EA service URL; use -url for that. "
|
||||
"Force the use of an adapter with the specified network. Must also provide -subnet.",
|
||||
.display_name = "netadapterip",
|
||||
.aliases = "netadapterip",
|
||||
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||
"with the specified IP address. You must also set -netadaptersubnet.",
|
||||
.type = OptionType::Text,
|
||||
.category = "Advanced Network",
|
||||
.sensitive = true,
|
||||
},
|
||||
{
|
||||
.title = "Preferred NetAdapter Subnet",
|
||||
.title = "Preferred Network Adapter's Subnet",
|
||||
.name = "subnet",
|
||||
.desc = "Force the use of an adapter with the specified subnet. Must also provide -network.",
|
||||
.display_name = "netadaptersubnet",
|
||||
.aliases = "netadaptersubnet",
|
||||
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||
"with the specified subnet. You must also set -netadapterip.",
|
||||
.type = OptionType::Text,
|
||||
.category = "Advanced Network",
|
||||
},
|
||||
@@ -505,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
{
|
||||
.title = "Overlay Keyboard Navigation",
|
||||
.name = "keyboardnav",
|
||||
.desc = "Enables keyboard navigation in the in-game overlay.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
{
|
||||
// OverlayScaling
|
||||
.title = "Spice Overlay UI Scale %",
|
||||
@@ -950,13 +962,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Cab Peripherals",
|
||||
},
|
||||
{
|
||||
.title = "SDVX FS Subscreen Native Touch Handling",
|
||||
.title = "SDVX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sdvxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -1031,16 +1042,16 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||
.name = "sdvxnolive2d",
|
||||
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||
"Off: leave Live2D as-is.\n"
|
||||
"Always: never render Live2D (also removes the menu navigator).\n"
|
||||
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||
"off: leave Live2D as-is.\n"
|
||||
"always: hide Live2D (also hides menu navigator).\n"
|
||||
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
.elements = {
|
||||
{"off", "Show Live2D"},
|
||||
{"always", "Never show Live2D"},
|
||||
{"ingame", "Show navigators only"},
|
||||
{"always", "Hide Live2D"},
|
||||
{"ingame", "Hide during songs"},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
@@ -1147,13 +1158,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// PopnNativeTouch
|
||||
.title = "Pop'n Music PikaPika Native Touch Handling",
|
||||
.title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "popnnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Pop'n Music",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -1772,11 +1782,11 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "I/O Modules",
|
||||
},
|
||||
{
|
||||
.title = "Disable Raw Input Touch",
|
||||
.title = "Touch Compatibility Mode (Disable Raw Input Touch)",
|
||||
.name = "wintouch",
|
||||
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
|
||||
"Win8 Pointer API or Win7 Touch API. Only enable this if you have trouble "
|
||||
"using the default (raw input) touch input, as Raw Input performs better.",
|
||||
"Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
|
||||
"touch screens, but may result in worse performance and higher latency.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
@@ -1796,6 +1806,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
{
|
||||
// RawInputTouchAspectRatio
|
||||
.title = "Raw Input Touch Fix Aspect Ratio",
|
||||
.name = "rawtouchaspect",
|
||||
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
|
||||
"different aspect ratio than the touch panel's native resolution. Only affects the default "
|
||||
"raw input touch handler. This is automatically enabled, only force on or off if you have trouble.\n\n"
|
||||
"auto (default): automatically enable on a per-game basis.\n"
|
||||
"on: forced on\n"
|
||||
"off: forced off",
|
||||
.type = OptionType::Enum,
|
||||
.category = "Touch Parameters",
|
||||
.elements = {
|
||||
{"auto", ""},
|
||||
{"on", ""},
|
||||
{"off", ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
// DisableTouchCardInsert
|
||||
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
||||
@@ -1807,12 +1835,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// spice2x_TouchCardInsert
|
||||
.title = "Show Insert Card button",
|
||||
.title = "Show Insert Card button (DEPRECATED)",
|
||||
.name = "sp2x-touchcard",
|
||||
.display_name = "touchcard",
|
||||
.aliases= "touchcard",
|
||||
.desc = "Show Insert Card touch button on main display.",
|
||||
.desc = "Show Insert Card touch button on main display. "
|
||||
"DEPRECATED - only works in very specific situations (jubeat + wintouch)",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
{
|
||||
@@ -1884,7 +1914,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.title = "HID SmartCard Fix UID",
|
||||
.name = "scardfix",
|
||||
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
||||
"legacy: Preserve buggy old behavior (Default)\n\n"
|
||||
"legacy (default): Preserve buggy old behavior\n\n"
|
||||
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
||||
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
||||
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
||||
@@ -2612,6 +2642,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Game Windowed Settings",
|
||||
.picker = OptionPickerType::Monitor,
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallSize
|
||||
.title = "GitaDora Windowed SMALL Size",
|
||||
.name = "gdwsmallsize",
|
||||
.desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "800,1280",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallPosition
|
||||
.title = "GitaDora Windowed SMALL Position",
|
||||
.name = "gdwsmallpos",
|
||||
.desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "0,0",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXWindowedSubscreenSize
|
||||
.title = "IIDX Windowed Subscreen Size",
|
||||
@@ -2668,6 +2718,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
|
||||
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
@@ -2675,10 +2726,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.elements = {
|
||||
{"legacy", ""},
|
||||
{"improved", ""},
|
||||
{"plus", ""},
|
||||
{"accurate", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// JubeatTouchDebug
|
||||
.title = "JB Touch Debug Overlay",
|
||||
.name = "jubeattouchdebug",
|
||||
.desc = "For touch screen players: draw a debug overlay on the main display. "
|
||||
"Requires the Spice Overlay to be enabled.\n\n"
|
||||
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
|
||||
"none - draw nothing\n\n"
|
||||
"box - show the 4x4 touch boundary boxes\n\n"
|
||||
"all - show both the boxes and the touch circles",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
.category = "Game Options",
|
||||
.elements = {
|
||||
{"auto", ""},
|
||||
{"none", ""},
|
||||
{"box", ""},
|
||||
{"all", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// JubeatTouchDebounce
|
||||
.title = "JB Touch Debounce",
|
||||
.name = "jubeattouchdebounce",
|
||||
.desc = "For touch screen players: ignore extremely quick touches by requiring a "
|
||||
"touch to be held for at least this many milliseconds before it registers.\n\n"
|
||||
"Useful for filtering phantom touches on a noisy touch screen. A higher value "
|
||||
"adds input latency, so keep it small. Default: off (0).",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "8",
|
||||
.game_name = "Jubeat",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// RBTouchDebug
|
||||
.title = "RB Touch Debug Overlay",
|
||||
.name = "rbtouchdebug",
|
||||
.desc = "Draw lines to show IR sensor emulation state.\n\n"
|
||||
"Note: lines will not perfectly align with touches; this is by design, it's showing the "
|
||||
"IR sensor state, before the game applies its own touch detection algorithm.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Reflec Beat",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// spice2x_RBTouchScale
|
||||
.title = "RB Touch Emulation Scale",
|
||||
@@ -2693,14 +2792,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// RBTouchSize
|
||||
.title = "RB Touch Emulation Size",
|
||||
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
|
||||
.name = "rbtouchsize",
|
||||
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
|
||||
.desc = "This option is deprecated and no longer has any effect. "
|
||||
"Reflec Beat touch emulation always uses the 3x3 sensor model.",
|
||||
.type = OptionType::Enum,
|
||||
.hidden = true,
|
||||
.game_name = "Reflec Beat",
|
||||
.category = "Game Options",
|
||||
.elements = {
|
||||
{"1", "1x1"},
|
||||
{"3", "3x3"},
|
||||
},
|
||||
},
|
||||
@@ -2708,9 +2808,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
// RBTouchPollRate
|
||||
.title = "RB Touch Emulation Poll Hz",
|
||||
.name = "rbtouchhz",
|
||||
.desc = "By default, the game polls for touch at 120Hz. "
|
||||
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
|
||||
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
|
||||
.desc = "By default, the game polls for touch at ~125Hz. "
|
||||
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
|
||||
"Higher rates reduce input latency (the game sees a fresher touch position) "
|
||||
"but do NOT improve spatial accuracy, and very high rates just waste CPU.",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "250",
|
||||
.game_name = "Reflec Beat",
|
||||
@@ -2787,15 +2888,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXNativeTouch
|
||||
.title = "IIDX TDJ Subscreen Native Touch Handling",
|
||||
.title = "IIDX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sp2x-iidxnativetouch",
|
||||
.display_name = "iidxnativetouch",
|
||||
.aliases= "iidxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Beatmania IIDX",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -3049,6 +3149,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// NostalgiaTouchMode
|
||||
.title = "Nostalgia Touch Piano",
|
||||
.name = "nosttouch",
|
||||
.desc =
|
||||
"Allows you to play the piano by touching the screen instead of a controller. "
|
||||
"Use the mode switch button to toggle between interacting with the menu and playing the piano.\n\n"
|
||||
"Velocity sensitivity is not supported. Touch targets will be pixel-perfect aligned with the "
|
||||
"judgement line, key beams, and notes.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// ForceBackBufferCount
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace launcher {
|
||||
SOFTID,
|
||||
VREnable,
|
||||
DisableOverlay,
|
||||
OverlayKeyboardNavigation,
|
||||
OverlayScaling,
|
||||
NotificationPosition,
|
||||
spice2x_FpsAutoShow,
|
||||
@@ -179,6 +180,7 @@ namespace launcher {
|
||||
ForceWinTouch,
|
||||
ForceTouchEmulation,
|
||||
InvertTouchCoordinates,
|
||||
RawInputTouchAspectRatio,
|
||||
DisableTouchCardInsert,
|
||||
spice2x_TouchCardInsert,
|
||||
ICCAReaderPort,
|
||||
@@ -256,12 +258,17 @@ namespace launcher {
|
||||
GitaDoraWindowedLeftMonitor,
|
||||
GitaDoraWindowedRightMonitor,
|
||||
GitaDoraWindowedSmallMonitor,
|
||||
GitaDoraWindowedSmallSize,
|
||||
GitaDoraWindowedSmallPosition,
|
||||
spice2x_IIDXWindowedSubscreenSize,
|
||||
spice2x_IIDXWindowedSubscreenPosition,
|
||||
IIDXWindowedSubscreenBorderless,
|
||||
IIDXWindowedSubscreenAlwaysOnTop,
|
||||
spice2x_JubeatLegacyTouch,
|
||||
JubeatTouchAlgo,
|
||||
JubeatTouchDebug,
|
||||
JubeatTouchDebounce,
|
||||
RBTouchDebug,
|
||||
spice2x_RBTouchScale,
|
||||
RBTouchSize,
|
||||
RBTouchPollRate,
|
||||
@@ -290,6 +297,7 @@ namespace launcher {
|
||||
DDRP4IOBufferMode,
|
||||
InputRequiresFocus,
|
||||
NostalgiaPoke,
|
||||
NostalgiaTouchMode,
|
||||
ForceBackBufferCount,
|
||||
SDVXWindowedSubscreenSize,
|
||||
SDVXWindowedSubscreenPosition,
|
||||
|
||||
+34
-32
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -382,52 +383,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 29;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 21;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 17;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 22;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 18;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 23;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 19;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 26;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
|
||||
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -455,52 +457,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 5;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 1;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 6;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 2;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 7;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 3;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 16;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 4;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "wintouchemu.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#define TOUCH_SIMULATE_FAT_FINGERS 0
|
||||
#define TOUCH_DEBUG_VERBOSE 0
|
||||
|
||||
#if TOUCH_DEBUG_VERBOSE
|
||||
#define log_debug(module, format_str, ...) logger::push( \
|
||||
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
|
||||
#else
|
||||
#define log_debug(module, format_str, ...)
|
||||
#endif
|
||||
|
||||
namespace nativetouchhook {
|
||||
|
||||
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||
|
||||
static void strip_contact_size(PTOUCHINPUT point) {
|
||||
|
||||
#if TOUCH_SIMULATE_FAT_FINGERS
|
||||
point->dwMask |= 0x004;
|
||||
point->cxContact = 80 * 100;
|
||||
point->cyContact = 60 * 100;
|
||||
#endif
|
||||
|
||||
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
|
||||
// monitors that do set it, IIDX can get very confused (SDVX is not
|
||||
// affected)
|
||||
//
|
||||
// while the test menu and the touch "glow" seem to work properly,
|
||||
// interacting with subscreen menu items or entering PIN becomes
|
||||
// very unpredictable
|
||||
//
|
||||
// to fix this, simply remove the contact area width and height
|
||||
//
|
||||
// note: test menu > I/O > touch test gives 5 numbers:
|
||||
// n: x, y, w, h
|
||||
// where
|
||||
// n is the nth touch input since boot
|
||||
// x, y are coordinates (center of finger)
|
||||
// w, h are contact width and height
|
||||
//
|
||||
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
|
||||
// automatically be seen as 1x1, which works perfectly fine
|
||||
|
||||
log_debug(
|
||||
"touch::native",
|
||||
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
|
||||
point->x / 100,
|
||||
point->y / 100,
|
||||
point->dwMask,
|
||||
point->cxContact,
|
||||
point->cyContact);
|
||||
|
||||
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
|
||||
point->cxContact = 0;
|
||||
point->cyContact = 0;
|
||||
}
|
||||
|
||||
static void flip_touch_points(PTOUCHINPUT point) {
|
||||
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
||||
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
||||
}
|
||||
|
||||
static BOOL WINAPI GetTouchInputInfoHook(
|
||||
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||
|
||||
// call the original fist
|
||||
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
||||
if (result == 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool flip_values = false;
|
||||
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
|
||||
log_debug(
|
||||
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
|
||||
rawinput::touch::DISPLAY_ORIENTATION,
|
||||
rawinput::touch::DISPLAY_SIZE_X,
|
||||
rawinput::touch::DISPLAY_SIZE_Y);
|
||||
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
||||
flip_values = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawinput::touch::INVERTED) {
|
||||
flip_values = !flip_values;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cInputs; i++) {
|
||||
PTOUCHINPUT point = &pInputs[i];
|
||||
|
||||
if (avs::game::is_model("LDJ")) {
|
||||
strip_contact_size(point);
|
||||
}
|
||||
|
||||
if (flip_values) {
|
||||
flip_touch_points(point);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void hook(HMODULE module) {
|
||||
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
||||
if (GetTouchInputInfo_orig != nullptr) {
|
||||
log_misc("touch::native", "GetTouchInputInfo hooked");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouchhook {
|
||||
void hook(HMODULE module);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ static cardunit_card_cardnumber_t cardunit_card_cardnumber = nullptr;
|
||||
static HINSTANCE SCIUNIT_INSTANCE;
|
||||
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
|
||||
static bool SCIUNIT_INITIALIZED = false;
|
||||
static int SCIUNIT_STATUS = 0;
|
||||
static bool CARD_IN = false;
|
||||
static bool CARD_PRESSED = false;
|
||||
static uint8_t CARD_UID[8];
|
||||
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_errorunit() {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_stat() {
|
||||
return 0;
|
||||
return SCIUNIT_STATUS;
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_version(int a1, int a2) {
|
||||
@@ -163,6 +164,7 @@ static int __cdecl sciunit_get_version(int a1, int a2) {
|
||||
|
||||
static int __cdecl sciunit_initialize() {
|
||||
SCIUNIT_INITIALIZED = true;
|
||||
SCIUNIT_STATUS = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_update() {
|
||||
if (SCIUNIT_INITIALIZED)
|
||||
if (SCIUNIT_INITIALIZED) {
|
||||
if (SCIUNIT_STATUS == 1) {
|
||||
SCIUNIT_STATUS = 2;
|
||||
}
|
||||
update_card();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,44 +4,19 @@
|
||||
|
||||
#include "wintouchemu.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "cfg/screen_resize.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/popn/popn.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/windows/generic_sub.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
#include "rawinput/touch.h"
|
||||
|
||||
#include "avs/game.h"
|
||||
|
||||
namespace wintouchemu {
|
||||
|
||||
typedef struct {
|
||||
POINT pos;
|
||||
bool last_button_pressed;
|
||||
DWORD touch_event;
|
||||
} mouse_state_t;
|
||||
|
||||
// settings
|
||||
bool FORCE = false;
|
||||
bool INJECT_MOUSE_AS_WM_TOUCH = false;
|
||||
bool LOG_FPS = false;
|
||||
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
||||
|
||||
// state
|
||||
double last_touch_event = 0.0;
|
||||
|
||||
static inline bool is_emu_enabled() {
|
||||
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
||||
}
|
||||
@@ -81,16 +56,13 @@ namespace wintouchemu {
|
||||
|
||||
// state
|
||||
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
|
||||
bool USE_MOUSE = false;
|
||||
std::vector<TouchEvent> TOUCH_EVENTS;
|
||||
std::vector<TouchPoint> TOUCH_POINTS;
|
||||
HMODULE HOOKED_MODULE = nullptr;
|
||||
std::string WINDOW_TITLE_START = "";
|
||||
std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
|
||||
volatile bool INITIALIZED = false;
|
||||
mouse_state_t mouse_state;
|
||||
bool INITIALIZED = false;
|
||||
|
||||
void hook(const char *window_title, HMODULE module, int delay_in_s) {
|
||||
void hook(const char *window_title, HMODULE module) {
|
||||
|
||||
// hooks
|
||||
auto system_metrics_hook = detour::iat_try(
|
||||
@@ -109,31 +81,8 @@ namespace wintouchemu {
|
||||
// set module and title
|
||||
HOOKED_MODULE = module;
|
||||
WINDOW_TITLE_START = window_title;
|
||||
|
||||
if (0 < delay_in_s) {
|
||||
// some games crash when touch events are injected too early during boot
|
||||
std::thread t([&]() {
|
||||
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
|
||||
log_misc("wintouchemu", "initializing", delay_in_s);
|
||||
INITIALIZED = true;
|
||||
});
|
||||
t.detach();
|
||||
} else {
|
||||
log_misc("wintouchemu", "initializing");
|
||||
INITIALIZED = true;
|
||||
}
|
||||
}
|
||||
|
||||
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
|
||||
hook(window_title_start, module);
|
||||
|
||||
WINDOW_TITLE_END = window_title_end;
|
||||
}
|
||||
|
||||
static void flip_touch_points(PTOUCHINPUT point) {
|
||||
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
||||
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
||||
log_misc("wintouchemu", "initializing");
|
||||
INITIALIZED = true;
|
||||
}
|
||||
|
||||
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||
@@ -145,7 +94,6 @@ namespace wintouchemu {
|
||||
|
||||
// set touch inputs
|
||||
bool result = false;
|
||||
bool mouse_used = false;
|
||||
for (UINT input = 0; input < cInputs; input++) {
|
||||
auto *touch_input = &pInputs[input];
|
||||
|
||||
@@ -178,13 +126,7 @@ namespace wintouchemu {
|
||||
|
||||
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
|
||||
|
||||
if (GRAPHICS_IIDX_WSUB) {
|
||||
// touch was received on subscreen window.
|
||||
RECT clientRect {};
|
||||
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
|
||||
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
|
||||
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
|
||||
} else if (overlay::OVERLAY) {
|
||||
if (overlay::OVERLAY) {
|
||||
// touch was received on global coords
|
||||
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
|
||||
} else {
|
||||
@@ -230,89 +172,10 @@ namespace wintouchemu {
|
||||
touch_input->cxContact = 0;
|
||||
touch_input->cyContact = 0;
|
||||
|
||||
if (avs::game::is_model("KFC") &&
|
||||
rawinput::touch::DISPLAY_INITIALIZED &&
|
||||
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
||||
flip_touch_points(touch_input);
|
||||
}
|
||||
|
||||
} else if (USE_MOUSE && !mouse_used) {
|
||||
|
||||
// disable further mouse inputs this call
|
||||
mouse_used = true;
|
||||
|
||||
if (mouse_state.touch_event) {
|
||||
result = true;
|
||||
touch_input->x = mouse_state.pos.x;
|
||||
touch_input->y = mouse_state.pos.y;
|
||||
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
touch_input->x -= SPICETOUCH_TOUCH_X;
|
||||
touch_input->y -= SPICETOUCH_TOUCH_Y;
|
||||
}
|
||||
|
||||
// log_misc(
|
||||
// "wintouchemu",
|
||||
// "mouse state ({}, {}) event={}",
|
||||
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
|
||||
|
||||
auto valid = true;
|
||||
if (overlay::OVERLAY) {
|
||||
valid = overlay::OVERLAY->transform_touch_point(
|
||||
&touch_input->x, &touch_input->y);
|
||||
}
|
||||
|
||||
// touch inputs require 100x precision per pixel
|
||||
touch_input->x *= 100;
|
||||
touch_input->y *= 100;
|
||||
touch_input->hSource = hTouchInput;
|
||||
touch_input->dwID = 0;
|
||||
touch_input->dwFlags = 0;
|
||||
switch (mouse_state.touch_event) {
|
||||
case TOUCHEVENTF_DOWN:
|
||||
if (valid) {
|
||||
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||
}
|
||||
|
||||
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
||||
}
|
||||
break;
|
||||
case TOUCHEVENTF_MOVE:
|
||||
if (valid) {
|
||||
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||
}
|
||||
|
||||
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
||||
}
|
||||
break;
|
||||
case TOUCHEVENTF_UP:
|
||||
// don't check valid so that this touch ID can be released
|
||||
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||
}
|
||||
|
||||
touch_input->dwFlags |= TOUCHEVENTF_UP;
|
||||
break;
|
||||
}
|
||||
touch_input->dwMask = 0;
|
||||
touch_input->dwTime = 0;
|
||||
touch_input->dwExtraInfo = 0;
|
||||
touch_input->cxContact = 0;
|
||||
touch_input->cyContact = 0;
|
||||
|
||||
// reset it since the event was consumed & propagated as touch
|
||||
mouse_state.touch_event = 0;
|
||||
}
|
||||
} else if (!GRAPHICS_IIDX_WSUB) {
|
||||
|
||||
/*
|
||||
* For some reason, Nostalgia won't show an active touch point unless a move event
|
||||
* triggers in the same frame. To work around this, we just supply a fake move
|
||||
* event if we didn't update the same pointer ID in the same call.
|
||||
*/
|
||||
} else {
|
||||
|
||||
// beatstream requires a MOVE for active points in each update
|
||||
// add one for every active point without a matching input event
|
||||
// find touch point which has no associated input event
|
||||
TouchPoint *touch_point = nullptr;
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
@@ -380,21 +243,6 @@ namespace wintouchemu {
|
||||
title = get_window_title(hWnd);
|
||||
}
|
||||
|
||||
// if a window title end is set, check to see if it matches
|
||||
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||||
hWnd = nullptr;
|
||||
title = "";
|
||||
|
||||
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
|
||||
auto check_title = get_window_title(window);
|
||||
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||||
hWnd = std::move(window);
|
||||
title = std::move(check_title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check window
|
||||
if (hWnd == nullptr) {
|
||||
return;
|
||||
@@ -402,39 +250,13 @@ namespace wintouchemu {
|
||||
|
||||
// check if windowed
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
if (GRAPHICS_IIDX_WSUB) {
|
||||
// no handling is needed here
|
||||
// graphics::MoveWindow_hook will attach hook to windowed subscreen
|
||||
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
|
||||
USE_MOUSE = false;
|
||||
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
|
||||
// overlay subscreen in IIDX
|
||||
// use mouse position as ImGui overlay will block the touch window
|
||||
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
|
||||
USE_MOUSE = true;
|
||||
} else if (games::popn::is_pikapika_model()) {
|
||||
// same as iidx case above
|
||||
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
|
||||
USE_MOUSE = true;
|
||||
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
|
||||
USE_MOUSE = true;
|
||||
} else {
|
||||
// create touch window - create overlay if not yet existing at this point
|
||||
log_info("wintouchemu", "create touch window relative to main game window");
|
||||
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
|
||||
USE_MOUSE = false;
|
||||
}
|
||||
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
|
||||
log_info(
|
||||
"wintouchemu",
|
||||
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
|
||||
USE_MOUSE = true;
|
||||
// create touch window - create overlay if not yet existing at this point
|
||||
log_info("wintouchemu", "create touch window relative to main game window");
|
||||
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
|
||||
} else {
|
||||
log_info("wintouchemu", "activating DirectX hooks");
|
||||
// mouse position based input only
|
||||
touch_attach_dx_hook();
|
||||
USE_MOUSE = false;
|
||||
}
|
||||
|
||||
// hooks
|
||||
@@ -445,8 +267,6 @@ namespace wintouchemu {
|
||||
}
|
||||
}
|
||||
|
||||
const auto now = get_performance_milliseconds();
|
||||
|
||||
// update touch events
|
||||
if (hWnd != nullptr) {
|
||||
|
||||
@@ -466,56 +286,11 @@ namespace wintouchemu {
|
||||
|
||||
// check if new events are available
|
||||
if (event_count > 0) {
|
||||
last_touch_event = now;
|
||||
|
||||
// send fake event to make the game update it's touch inputs
|
||||
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
||||
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
|
||||
}
|
||||
|
||||
// update frame logging
|
||||
if (LOG_FPS) {
|
||||
static int log_frames = 0;
|
||||
static uint64_t log_time = 0;
|
||||
log_frames++;
|
||||
if (log_time < get_system_seconds()) {
|
||||
if (log_time > 0) {
|
||||
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
|
||||
}
|
||||
log_frames = 0;
|
||||
log_time = get_system_seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send separate WM_TOUCH event for mouse
|
||||
// this must be separate from actual touch events because some games will ignore the return
|
||||
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
|
||||
// send empty events when the mouse button is not clicked/released
|
||||
if (hWnd != nullptr && USE_MOUSE) {
|
||||
bool button_pressed = get_async_primary_mouse();
|
||||
|
||||
// if there was a touch event in the last 500 ms, don't insert new button presses
|
||||
if (button_pressed && (now - last_touch_event) < 500) {
|
||||
button_pressed = false;
|
||||
}
|
||||
|
||||
// figure out what kind of touch event to simulate
|
||||
if (button_pressed && !mouse_state.last_button_pressed) {
|
||||
mouse_state.touch_event = TOUCHEVENTF_DOWN;
|
||||
} else if (button_pressed && mouse_state.last_button_pressed) {
|
||||
mouse_state.touch_event = TOUCHEVENTF_MOVE;
|
||||
} else if (!button_pressed && mouse_state.last_button_pressed) {
|
||||
mouse_state.touch_event = TOUCHEVENTF_UP;
|
||||
}
|
||||
|
||||
mouse_state.last_button_pressed = button_pressed;
|
||||
if (mouse_state.touch_event) {
|
||||
GetCursorPos(&mouse_state.pos);
|
||||
// send fake event to make the game update it's touch inputs
|
||||
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
||||
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,8 @@ namespace wintouchemu {
|
||||
|
||||
// settings
|
||||
extern bool FORCE;
|
||||
extern bool INJECT_MOUSE_AS_WM_TOUCH;
|
||||
extern bool LOG_FPS;
|
||||
extern bool ADD_TOUCH_FLAG_PRIMARY;
|
||||
|
||||
void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0);
|
||||
void hook_title_ends(
|
||||
const char *window_title_start,
|
||||
const char *window_title_ends,
|
||||
HMODULE module = nullptr);
|
||||
void hook(const char *window_title, HMODULE module = nullptr);
|
||||
void update();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include "games/io.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/rb/touch_debug.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
@@ -50,6 +52,7 @@
|
||||
#include "windows/sdvx_sub.h"
|
||||
#include "windows/keypad.h"
|
||||
#include "windows/log.h"
|
||||
#include "windows/nostalgia_touch_piano.h"
|
||||
#include "windows/obs.h"
|
||||
#include "windows/patch_manager.h"
|
||||
#include "windows/exitprompt.cpp"
|
||||
@@ -66,6 +69,7 @@ namespace overlay {
|
||||
bool AUTO_SHOW_IOPANEL = false;
|
||||
bool AUTO_SHOW_KEYPAD_P1 = false;
|
||||
bool AUTO_SHOW_KEYPAD_P2 = false;
|
||||
bool ENABLE_KEYBOARD_NAVIGATION = false;
|
||||
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
|
||||
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
|
||||
std::optional<uint32_t> UI_SCALE_PERCENT;
|
||||
@@ -311,6 +315,9 @@ void overlay::SpiceOverlay::init() {
|
||||
auto &io = ImGui::GetIO();
|
||||
io.UserData = this;
|
||||
io.ConfigFlags = ImGuiConfigFlags_None;
|
||||
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
}
|
||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
}
|
||||
@@ -399,6 +406,12 @@ void overlay::SpiceOverlay::init() {
|
||||
window_fps->set_active(true);
|
||||
}
|
||||
|
||||
if (!cfg::CONFIGURATOR_STANDALONE &&
|
||||
avs::game::is_model("PAN") && games::nost::ENABLE_TOUCH_MODE) {
|
||||
window_nostalgia_touch_piano =
|
||||
std::make_unique<overlay::windows::NostalgiaTouchPiano>(this);
|
||||
}
|
||||
|
||||
// owned separately from `windows` so it is not part of the overlay window layer
|
||||
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
|
||||
|
||||
@@ -551,9 +564,18 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_fps->get_active();
|
||||
|
||||
const bool draw_nostalgia_touch_piano = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_nostalgia_touch_piano
|
||||
&& this->window_nostalgia_touch_piano->get_active();
|
||||
|
||||
// draw RB touch diagnostics
|
||||
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& games::rb::touch_debug_overlay_enabled();
|
||||
|
||||
// check if there is nothing to draw
|
||||
this->has_pending_frame = false;
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent) {
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent &&
|
||||
!draw_nostalgia_touch_piano && !draw_rb_touch_debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,12 +600,22 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
ImGui::NewFrame();
|
||||
this->has_pending_frame = true;
|
||||
|
||||
if (draw_rb_touch_debug) {
|
||||
games::rb::touch_draw_debug_overlay();
|
||||
}
|
||||
|
||||
// build windows only when the overlay itself is active
|
||||
if (this->active) {
|
||||
for (auto &window : this->windows) {
|
||||
window->build();
|
||||
}
|
||||
}
|
||||
|
||||
if (draw_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->build();
|
||||
}
|
||||
|
||||
if (this->active) {
|
||||
// draw the main menu on top of the overlay windows
|
||||
this->window_main_menu->build();
|
||||
|
||||
@@ -595,6 +627,7 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
if (draw_fps_persistent) {
|
||||
this->window_fps->build();
|
||||
}
|
||||
|
||||
// draw notifications last so they paint on top of any overlay windows
|
||||
if (draw_notifications) {
|
||||
overlay::notifications::draw();
|
||||
@@ -805,6 +838,10 @@ void overlay::SpiceOverlay::update() {
|
||||
// FPS window
|
||||
this->window_fps->update();
|
||||
|
||||
if (this->window_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->update();
|
||||
}
|
||||
|
||||
// main menu (owned separately from the overlay window layer)
|
||||
this->window_main_menu->update();
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace overlay {
|
||||
extern bool AUTO_SHOW_IOPANEL;
|
||||
extern bool AUTO_SHOW_KEYPAD_P1;
|
||||
extern bool AUTO_SHOW_KEYPAD_P2;
|
||||
extern bool ENABLE_KEYBOARD_NAVIGATION;
|
||||
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
|
||||
extern FpsLocation FPS_LOCATION;
|
||||
extern bool SHOW_DEBUG_LOG_WINDOW;
|
||||
@@ -82,6 +83,7 @@ namespace overlay {
|
||||
// not part of `windows`: drawn/updated on the persistent layer (like
|
||||
// notifications), independent of the overlay's active state and input gates.
|
||||
std::unique_ptr<Window> window_fps;
|
||||
std::unique_ptr<Window> window_nostalgia_touch_piano;
|
||||
|
||||
// not part of `windows`: the main menu / launcher. owned and drawn
|
||||
// separately from the overlay window layer; it drives the overlay's
|
||||
@@ -137,6 +139,9 @@ namespace overlay {
|
||||
inline IDirect3DDevice9 *get_device() {
|
||||
return this->device;
|
||||
}
|
||||
inline HWND get_window() {
|
||||
return this->hWnd;
|
||||
}
|
||||
|
||||
bool can_transform_touch_input() {
|
||||
return (this->subscreen_mouse_handler != nullptr);
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace overlay::windows {
|
||||
static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = {
|
||||
{ "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS },
|
||||
{ "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS },
|
||||
{ "Modifiers", ControllerPage::CONTROLLER_PAGE_MODIFIERS },
|
||||
{ "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS },
|
||||
{ "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY },
|
||||
{ "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS },
|
||||
@@ -251,7 +252,7 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
// Renders an arrow-less, non-collapsible left-nav header row, highlighted when
|
||||
// active. Returns true if the row was clicked this frame; callers apply their
|
||||
// active. Returns true if the row was activated this frame; callers apply their
|
||||
// own selection side effects.
|
||||
bool Config::build_nav_header(const char *label, bool active) {
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf;
|
||||
@@ -282,7 +283,7 @@ namespace overlay::windows {
|
||||
ImGui::PopStyleColor(colors_pushed);
|
||||
}
|
||||
|
||||
return ImGui::IsItemClicked();
|
||||
return ImGui::IsItemActivated();
|
||||
}
|
||||
|
||||
void Config::build_options_tab(float page_offset) {
|
||||
@@ -616,6 +617,21 @@ namespace overlay::windows {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ControllerPage::CONTROLLER_PAGE_MODIFIERS: {
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Modifiers");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Bind modifier buttons below, then open Edit properties on any button binding "
|
||||
"(except MIDI) to select the required modifiers. This allows you to configure "
|
||||
"button combinations.\n\n"
|
||||
"For example, if you bind the Start button as Modifier 1, and then bind "
|
||||
"the A button for Service Coin and set it to require Modifier 1, you press "
|
||||
"Start + A to trigger Service Coin.");
|
||||
ImGui::TextUnformatted("");
|
||||
this->build_buttons(
|
||||
"Modifier", games::get_buttons_modifiers(this->games_selected_name));
|
||||
break;
|
||||
}
|
||||
case ControllerPage::CONTROLLER_PAGE_ANALOGS: {
|
||||
|
||||
// help text for binding analog, if the game has one
|
||||
@@ -1256,7 +1272,8 @@ namespace overlay::windows {
|
||||
if (open_edit) {
|
||||
ImGui::OpenPopup(edit_name.c_str());
|
||||
}
|
||||
edit_button_popup(edit_name, button_display, button, button_velocity, alt_index);
|
||||
edit_button_popup(
|
||||
edit_name, button_display, button, button_velocity, alt_index, name != "Modifier");
|
||||
|
||||
// clean up
|
||||
ImGui::PopID();
|
||||
@@ -1275,6 +1292,7 @@ namespace overlay::windows {
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setInvert(false);
|
||||
button->setModifierMask(0);
|
||||
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
|
||||
button->setLastVelocity(0);
|
||||
button->setTemporary(false);
|
||||
@@ -1356,8 +1374,9 @@ namespace overlay::windows {
|
||||
if (check_devices) {
|
||||
// iterate updated devices
|
||||
auto updated_devices = RI_MGR->devices_get_updated();
|
||||
bool binding_finished = false;
|
||||
for (auto device : updated_devices) {
|
||||
std::lock_guard<std::mutex> lock(*device->mutex);
|
||||
std::unique_lock<std::mutex> lock(*device->mutex);
|
||||
switch (device->type) {
|
||||
case rawinput::MOUSE: {
|
||||
auto mouse = device->mouseInfo;
|
||||
@@ -1369,12 +1388,13 @@ namespace overlay::windows {
|
||||
button->setDeviceIdentifier(device->name);
|
||||
button->setVKey(static_cast<unsigned short>(i));
|
||||
button->setAnalogType(BAT_NONE);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1393,7 +1413,7 @@ namespace overlay::windows {
|
||||
buttons_bind_active = false;
|
||||
buttons_many_index = -1;
|
||||
buttons_many_active = false;
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1401,12 +1421,13 @@ namespace overlay::windows {
|
||||
button->setDeviceIdentifier(device->name);
|
||||
button->setVKey(vkey);
|
||||
button->setAnalogType(BAT_NONE);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1536,13 +1557,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1581,6 +1603,7 @@ namespace overlay::windows {
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
// same idea as setMidiVKey - keep velocity threshold consistent
|
||||
button->setVelocityThreshold(
|
||||
device->midiInfo->v2_velocity_threshold[button->getVKey()]);
|
||||
@@ -1590,7 +1613,7 @@ namespace overlay::windows {
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1608,13 +1631,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1635,13 +1659,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1662,13 +1687,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1688,13 +1714,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1709,13 +1736,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1739,13 +1767,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1755,6 +1784,13 @@ namespace overlay::windows {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lock.unlock();
|
||||
if (binding_finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (binding_finished) {
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1808,6 +1844,7 @@ namespace overlay::windows {
|
||||
if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) {
|
||||
button->setDeviceIdentifier(xinput::get_device_desc(xinput.player));
|
||||
button->setVKey(static_cast<uint16_t>(xinput.button));
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
inc_buttons_many_index(button_it_max);
|
||||
@@ -1864,6 +1901,7 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
@@ -1882,27 +1920,103 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::build_modifier_picker(Button &button) {
|
||||
bool changed = false;
|
||||
auto modifier_mask = button.getModifierMask();
|
||||
|
||||
// collect live states and build the closed dropdown summary
|
||||
uint8_t pressed_modifier_mask = 0;
|
||||
std::string modifier_preview;
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
const uint8_t modifier_bit = UINT8_C(1) << index;
|
||||
if (modifier_buttons && index < modifier_buttons->size() &&
|
||||
GameAPI::Buttons::getState(RI_MGR, modifier_buttons->at(index))) {
|
||||
pressed_modifier_mask |= modifier_bit;
|
||||
}
|
||||
if ((modifier_mask & modifier_bit) != 0) {
|
||||
modifier_preview += modifier_preview.empty() ? "Mod " : "+";
|
||||
modifier_preview += std::to_string(index + 1);
|
||||
}
|
||||
}
|
||||
if (modifier_preview.empty()) {
|
||||
modifier_preview = "None";
|
||||
}
|
||||
|
||||
// color the combo when all requirements are held, but keep its label unchanged
|
||||
const bool all_modifiers_pressed = modifier_mask != 0 &&
|
||||
(pressed_modifier_mask & modifier_mask) == modifier_mask;
|
||||
if (all_modifiers_pressed) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
const bool combo_open = ImGui::BeginCombo("##Modifiers", modifier_preview.c_str());
|
||||
if (all_modifiers_pressed) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// update requirements while highlighting modifier sources that are currently pressed
|
||||
if (combo_open) {
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
const uint8_t modifier_bit = UINT8_C(1) << index;
|
||||
bool required = (modifier_mask & modifier_bit) != 0;
|
||||
const auto label = fmt::format("Modifier {}", index + 1);
|
||||
const bool modifier_pressed = (pressed_modifier_mask & modifier_bit) != 0;
|
||||
if (modifier_pressed) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
const bool requirement_changed = ImGui::Checkbox(label.c_str(), &required);
|
||||
if (modifier_pressed) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
if (requirement_changed) {
|
||||
modifier_mask ^= modifier_bit;
|
||||
button.setModifierMask(modifier_mask);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
|
||||
ImGui::TextUnformatted("Modifiers");
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void Config::edit_button_popup(
|
||||
const std::string &edit_name,
|
||||
const std::string &button_display,
|
||||
Button *button,
|
||||
const float button_velocity,
|
||||
const int alt_index) {
|
||||
const int alt_index,
|
||||
const bool allow_modifiers) {
|
||||
|
||||
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
|
||||
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
bool dirty = false;
|
||||
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
|
||||
|
||||
auto switch_device = [&](const std::string &device_identifier) {
|
||||
button->setDeviceIdentifier(device_identifier);
|
||||
button->setAnalogType(ButtonAnalogType::BAT_NONE);
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setVKey(0);
|
||||
button->setInvert(false);
|
||||
button->setModifierMask(0);
|
||||
dirty = true;
|
||||
};
|
||||
|
||||
// binding
|
||||
ImGui::Text("Binding");
|
||||
ImGui::Separator();
|
||||
|
||||
// combo for devices
|
||||
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
|
||||
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
|
||||
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) {
|
||||
button->setDeviceIdentifier("");
|
||||
dirty = true;
|
||||
if (ImGui::Selectable("Empty (Naive)", button->isNaive()) && !button->isNaive()) {
|
||||
switch_device("");
|
||||
}
|
||||
if (button->isNaive()) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -1910,17 +2024,8 @@ namespace overlay::windows {
|
||||
for (auto &device : RI_MGR->devices_get()) {
|
||||
bool selected = button->getDeviceIdentifier() == device.name.c_str();
|
||||
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
|
||||
if (ImGui::Selectable(device_desc.c_str(), selected)) {
|
||||
button->setDeviceIdentifier(device.name);
|
||||
// reset controls when switching devices
|
||||
button->setAnalogType(ButtonAnalogType::BAT_NONE);
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setVKey(0);
|
||||
button->setInvert(false);
|
||||
dirty = true;
|
||||
if (ImGui::Selectable(device_desc.c_str(), selected) && !selected) {
|
||||
switch_device(device.name);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -1928,6 +2033,7 @@ namespace overlay::windows {
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
device = RI_MGR->devices_get(button->getDeviceIdentifier());
|
||||
|
||||
// analog type (only for HID)
|
||||
const auto bat = button->getAnalogType();
|
||||
@@ -2219,21 +2325,22 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifiers
|
||||
{
|
||||
const bool is_midi = device != nullptr && device->type == rawinput::MIDI;
|
||||
if (allow_modifiers && button->isValid() && !is_midi) {
|
||||
dirty |= build_modifier_picker(*button);
|
||||
}
|
||||
}
|
||||
|
||||
// preview
|
||||
if (!button_display.empty()) {
|
||||
ImGui::TextUnformatted("\nPreview");
|
||||
ImGui::Separator();
|
||||
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
|
||||
}
|
||||
// this doesn't account for utf-8 but whatever
|
||||
if (button_display.size() >= 40) {
|
||||
ImGui::Text("%.37s...", button_display.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(button_display.c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(button_display.c_str());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
ImGui::TextTruncated(button_display, ImGui::GetContentRegionAvail().x);
|
||||
ImGui::TextUnformatted("\n");
|
||||
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2242,8 +2349,10 @@ namespace overlay::windows {
|
||||
ImGui::TextUnformatted("");
|
||||
}
|
||||
|
||||
|
||||
// options
|
||||
ImGui::Text("Options");
|
||||
ImGui::TextUnformatted("Options");
|
||||
ImGui::Separator();
|
||||
|
||||
// check for debounce
|
||||
if (button->getDebounceUp() || button->getDebounceDown()
|
||||
@@ -2362,6 +2471,7 @@ namespace overlay::windows {
|
||||
ImGui::ProgressBar(button_velocity);
|
||||
} else {
|
||||
ImGui::Text("State");
|
||||
ImGui::Separator();
|
||||
ImGui::ProgressBar(button_velocity);
|
||||
}
|
||||
|
||||
@@ -5482,6 +5592,12 @@ namespace overlay::windows {
|
||||
tmpl.buttons.emplace_back(btn);
|
||||
}
|
||||
}
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
if (modifier_buttons) {
|
||||
for (auto &modifier : *modifier_buttons) {
|
||||
tmpl.modifier_buttons.emplace_back(modifier);
|
||||
}
|
||||
}
|
||||
|
||||
// capture keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
@@ -5537,6 +5653,17 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// clear all modifier buttons
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
if (modifier_buttons) {
|
||||
for (auto &modifier : *modifier_buttons) {
|
||||
for (int ai = (int)modifier.getAlternatives().size() - 1; ai >= 0; ai--) {
|
||||
clear_button(&modifier.getAlternatives()[ai], ai + 1);
|
||||
}
|
||||
clear_button(&modifier, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// clear all keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
if (keypad_buttons) {
|
||||
@@ -5630,6 +5757,7 @@ namespace overlay::windows {
|
||||
btn.setDebounceDown(entry->debounce_down);
|
||||
btn.setBatThreshold(entry->bat_threshold);
|
||||
btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
btn.setModifierMask(entry->modifier_mask);
|
||||
::Config::getInstance().updateBinding(game, btn, -1);
|
||||
} else {
|
||||
Button alt_btn(btn.getName());
|
||||
@@ -5641,6 +5769,7 @@ namespace overlay::windows {
|
||||
alt_btn.setDebounceDown(entry->debounce_down);
|
||||
alt_btn.setBatThreshold(entry->bat_threshold);
|
||||
alt_btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
alt_btn.setModifierMask(entry->modifier_mask);
|
||||
alt_btn.setTemporary(true);
|
||||
btn.getAlternatives().push_back(alt_btn);
|
||||
::Config::getInstance().updateBinding(
|
||||
@@ -5659,6 +5788,11 @@ namespace overlay::windows {
|
||||
if (this->apply_buttons) {
|
||||
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name));
|
||||
}
|
||||
if (this->apply_modifiers) {
|
||||
apply_buttons(
|
||||
tmpl.modifier_buttons,
|
||||
games::get_buttons_modifiers(this->games_selected_name));
|
||||
}
|
||||
if (this->apply_keypads) {
|
||||
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
|
||||
}
|
||||
@@ -5900,7 +6034,7 @@ namespace overlay::windows {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(
|
||||
"Clears all game button, analog, and light bindings.");
|
||||
"Clears all game button, modifier, keypad, analog, and light bindings.");
|
||||
if (this->all_cleared) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Done.");
|
||||
@@ -5915,6 +6049,8 @@ namespace overlay::windows {
|
||||
bool selection_changed = false;
|
||||
selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Modifiers", &this->apply_modifiers);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs);
|
||||
@@ -5924,7 +6060,7 @@ namespace overlay::windows {
|
||||
std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false);
|
||||
}
|
||||
|
||||
if (!this->apply_buttons && !this->apply_keypads &&
|
||||
if (!this->apply_buttons && !this->apply_modifiers && !this->apply_keypads &&
|
||||
!this->apply_analogs && !this->apply_lights) {
|
||||
ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n");
|
||||
}
|
||||
@@ -5964,7 +6100,8 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
|
||||
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
|
||||
!apply_analogs && !apply_lights) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
@@ -6133,7 +6270,8 @@ namespace overlay::windows {
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
|
||||
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
|
||||
!apply_analogs && !apply_lights) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
@@ -6212,12 +6350,14 @@ namespace overlay::windows {
|
||||
ImGui::TextUnformatted("Pick which groups to save:");
|
||||
ImGui::Checkbox("Buttons", &this->save_buttons);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Modifiers", &this->save_modifiers);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Keypads", &this->save_keypads);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Analogs", &this->save_analogs);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Lights", &this->save_lights);
|
||||
if (!this->save_analogs && !this->save_keypads &&
|
||||
if (!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
|
||||
!this->save_analogs && !this->save_lights) {
|
||||
ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n");
|
||||
}
|
||||
@@ -6269,7 +6409,8 @@ namespace overlay::windows {
|
||||
|
||||
ImGui::BeginDisabled(
|
||||
!all_labels_set ||
|
||||
(!this->save_buttons && !this->save_keypads && !this->save_analogs && !this->save_lights));
|
||||
(!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
|
||||
!this->save_analogs && !this->save_lights));
|
||||
|
||||
if (ImGui::Button("Save")) {
|
||||
// replace device IDs with labels in the template
|
||||
@@ -6278,7 +6419,8 @@ namespace overlay::windows {
|
||||
template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
if (save_user_template(template_pending_save,
|
||||
this->save_buttons, this->save_keypads, this->save_analogs, this->save_lights)) {
|
||||
this->save_buttons, this->save_modifiers, this->save_keypads,
|
||||
this->save_analogs, this->save_lights)) {
|
||||
template_save_name[0] = '\0';
|
||||
templates_cache_dirty = true;
|
||||
}
|
||||
@@ -6360,7 +6502,7 @@ namespace overlay::windows {
|
||||
t.rename_source(template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
}
|
||||
save_user_template(t, true, true, true, true);
|
||||
save_user_template(t, true, true, true, true, true);
|
||||
templates_cache_dirty = true;
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
CONTROLLER_PAGE_INVALID,
|
||||
CONTROLLER_PAGE_BUTTONS,
|
||||
CONTROLLER_PAGE_KEYPADS,
|
||||
CONTROLLER_PAGE_MODIFIERS,
|
||||
CONTROLLER_PAGE_ANALOGS,
|
||||
CONTROLLER_PAGE_OVERLAY,
|
||||
CONTROLLER_PAGE_LIGHTS,
|
||||
@@ -119,10 +120,12 @@ namespace overlay::windows {
|
||||
std::vector<std::string> template_save_sources;
|
||||
std::vector<std::string> template_save_labels;
|
||||
bool apply_buttons = true;
|
||||
bool apply_modifiers = true;
|
||||
bool apply_keypads = true;
|
||||
bool apply_analogs = true;
|
||||
bool apply_lights = true;
|
||||
bool save_buttons = true;
|
||||
bool save_modifiers = true;
|
||||
bool save_keypads = true;
|
||||
bool save_analogs = true;
|
||||
bool save_lights = true;
|
||||
@@ -168,12 +171,14 @@ namespace overlay::windows {
|
||||
|
||||
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max, const int alt_index);
|
||||
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max, const int alt_index);
|
||||
bool build_modifier_picker(Button &button);
|
||||
void edit_button_popup(
|
||||
const std::string &edit_name,
|
||||
const std::string &button_display,
|
||||
Button *button,
|
||||
const float button_velocity,
|
||||
const int alt_index);
|
||||
const int alt_index,
|
||||
const bool allow_modifiers);
|
||||
void clear_button(Button *button, const int alt_index, std::optional<unsigned short> vKey_default = std::nullopt);
|
||||
void reset_button_to_default(Button *button, unsigned short vKey_default);
|
||||
unsigned int get_keypad_top_row(const Button &button);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// helpers - iterate both buttons and keypad_buttons
|
||||
// helpers - iterate all button groups
|
||||
static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.is_naive()) naive++;
|
||||
@@ -21,6 +21,7 @@ namespace overlay::windows {
|
||||
int ControllerTemplate::count_naive_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(modifier_buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return naive;
|
||||
}
|
||||
@@ -28,6 +29,7 @@ namespace overlay::windows {
|
||||
int ControllerTemplate::count_device_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(modifier_buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return device;
|
||||
}
|
||||
@@ -64,6 +66,7 @@ namespace overlay::windows {
|
||||
std::set<std::string> ControllerTemplate::get_used_devices() const {
|
||||
std::set<std::string> devices;
|
||||
collect_devices_from(buttons, devices);
|
||||
collect_devices_from(modifier_buttons, devices);
|
||||
collect_devices_from(keypad_buttons, devices);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) devices.insert(a.device_identifier);
|
||||
@@ -94,6 +97,7 @@ namespace overlay::windows {
|
||||
bool has_naive = false;
|
||||
|
||||
collect_sources_from(buttons, sources_set, has_naive);
|
||||
collect_sources_from(modifier_buttons, sources_set, has_naive);
|
||||
collect_sources_from(keypad_buttons, sources_set, has_naive);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) {
|
||||
@@ -131,6 +135,7 @@ namespace overlay::windows {
|
||||
}
|
||||
};
|
||||
rename_in_buttons(buttons);
|
||||
rename_in_buttons(modifier_buttons);
|
||||
rename_in_buttons(keypad_buttons);
|
||||
for (auto &a : analogs) {
|
||||
if (a.device_identifier == old_id) a.device_identifier = new_id;
|
||||
@@ -181,6 +186,16 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
std::vector<std::string> modifier_lines;
|
||||
collect_btn_lines(modifier_buttons, modifier_lines);
|
||||
if (!modifier_lines.empty()) {
|
||||
result += "Modifiers:\n";
|
||||
for (auto &line : modifier_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
std::vector<std::string> kp_lines;
|
||||
collect_btn_lines(keypad_buttons, kp_lines);
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace overlay::windows {
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0;
|
||||
unsigned short velocity_threshold = 0;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
|
||||
bool is_device() const { return !device_identifier.empty(); }
|
||||
@@ -34,6 +35,7 @@ namespace overlay::windows {
|
||||
e.debounce_down = btn.getDebounceDown();
|
||||
e.bat_threshold = btn.getBatThreshold();
|
||||
e.velocity_threshold = btn.getVelocityThreshold();
|
||||
e.modifier_mask = btn.getModifierMask();
|
||||
return e;
|
||||
}
|
||||
};
|
||||
@@ -123,6 +125,7 @@ namespace overlay::windows {
|
||||
std::string game_name;
|
||||
bool is_builtin = false;
|
||||
std::vector<TemplateButtonBinding> buttons;
|
||||
std::vector<TemplateButtonBinding> modifier_buttons;
|
||||
std::vector<TemplateButtonBinding> keypad_buttons;
|
||||
std::vector<TemplateAnalogBinding> analogs;
|
||||
std::vector<TemplateLightBinding> lights;
|
||||
@@ -150,6 +153,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights);
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "nostalgia_touch_piano.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "external/imgui/imgui_internal.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
static constexpr float BUTTON_WIDTH = 144.f;
|
||||
static constexpr float BUTTON_HEIGHT = 40.f;
|
||||
static constexpr float WINDOW_PADDING = 4.f;
|
||||
static constexpr float EDGE_MARGIN = 4.f;
|
||||
static constexpr float PIANO_HEIGHT_RATIO = 0.08f;
|
||||
static constexpr float PIANO_LEFT_GAP = 11.f;
|
||||
static constexpr float PIANO_RIGHT_GAP = 10.f;
|
||||
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||
|
||||
static constexpr ImU32 PIANO_KEY_COLOR = IM_COL32(255, 255, 255, 50);
|
||||
static constexpr ImU32 PIANO_KEY_ACTIVE_COLOR = IM_COL32(255, 48, 48, 160);
|
||||
static constexpr ImU32 PIANO_KEY_BORDER_COLOR = IM_COL32(0, 0, 0, 100);
|
||||
|
||||
struct ButtonPalette {
|
||||
ImVec4 normal;
|
||||
ImVec4 hovered;
|
||||
ImVec4 active;
|
||||
};
|
||||
|
||||
static const ButtonPalette NAV_MODE_PALETTE {
|
||||
ImVec4(0.10f, 0.45f, 0.28f, 0.72f),
|
||||
ImVec4(0.14f, 0.58f, 0.36f, 0.82f),
|
||||
ImVec4(0.08f, 0.34f, 0.21f, 0.90f),
|
||||
};
|
||||
static const ButtonPalette PIANO_MODE_PALETTE {
|
||||
ImVec4(0.15f, 0.32f, 0.62f, 0.72f),
|
||||
ImVec4(0.20f, 0.42f, 0.78f, 0.82f),
|
||||
ImVec4(0.10f, 0.24f, 0.50f, 0.90f),
|
||||
};
|
||||
|
||||
static void draw_piano_keys(
|
||||
const ImVec2 &display_size,
|
||||
LONG client_width,
|
||||
uint32_t key_state) {
|
||||
|
||||
if (display_size.x <= 0.f || display_size.y <= 0.f || client_width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is only a visual guide; native touch routing owns the actual input
|
||||
const float left_gap = PIANO_LEFT_GAP * display_size.x / client_width;
|
||||
const float right_gap = PIANO_RIGHT_GAP * display_size.x / client_width;
|
||||
const float piano_width = display_size.x - left_gap - right_gap;
|
||||
if (piano_width <= 0.f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float key_width = piano_width / PIANO_KEY_COUNT;
|
||||
const float key_top = display_size.y * (1.f - PIANO_HEIGHT_RATIO);
|
||||
auto *draw_list = ImGui::GetBackgroundDrawList();
|
||||
|
||||
for (uint32_t key = 0; key < PIANO_KEY_COUNT; key++) {
|
||||
const ImVec2 key_min(left_gap + key * key_width, key_top);
|
||||
const ImVec2 key_max(left_gap + (key + 1) * key_width, display_size.y);
|
||||
const bool active = (key_state & (UINT32_C(1) << key)) != 0;
|
||||
draw_list->AddRectFilled(
|
||||
key_min,
|
||||
key_max,
|
||||
active ? PIANO_KEY_ACTIVE_COLOR : PIANO_KEY_COLOR);
|
||||
draw_list->AddRect(key_min, key_max, PIANO_KEY_BORDER_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
NostalgiaTouchPiano::NostalgiaTouchPiano(SpiceOverlay *overlay) : Window(overlay) {
|
||||
this->title = "Nostalgia Touch Piano";
|
||||
this->flags = ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoCollapse
|
||||
| ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoDocking
|
||||
| ImGuiWindowFlags_NoBackground
|
||||
| ImGuiWindowFlags_NoSavedSettings
|
||||
| ImGuiWindowFlags_NoNav
|
||||
| ImGuiWindowFlags_NoBringToFrontOnFocus;
|
||||
this->window_padding = overlay::apply_scaling_to_vector(WINDOW_PADDING, WINDOW_PADDING);
|
||||
this->set_active(true);
|
||||
}
|
||||
|
||||
void NostalgiaTouchPiano::calculate_initial_window() {
|
||||
this->init_size = overlay::apply_scaling_to_vector(
|
||||
BUTTON_WIDTH + WINDOW_PADDING * 2,
|
||||
BUTTON_HEIGHT + WINDOW_PADDING * 2);
|
||||
this->init_pos = overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN);
|
||||
}
|
||||
|
||||
void NostalgiaTouchPiano::build_content() {
|
||||
// keep the control anchored while the game window changes size or mode
|
||||
ImGui::SetWindowPos(
|
||||
overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN),
|
||||
ImGuiCond_Always);
|
||||
|
||||
// stay above regular overlay windows, but never cover a blocking modal
|
||||
ImGuiWindow *mode_window = ImGui::GetCurrentWindow();
|
||||
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
|
||||
ImGui::BringWindowToDisplayBehind(mode_window, modal);
|
||||
} else {
|
||||
ImGui::BringWindowToDisplayFront(mode_window);
|
||||
}
|
||||
|
||||
const bool nav_mode =
|
||||
games::nost::touch_mode::current_mode() == games::nost::touch_mode::Mode::Nav;
|
||||
const char *label = nav_mode ? "Nav Mode" : "Piano Mode";
|
||||
|
||||
// make the active routing mode recognizable without reading the label
|
||||
const auto &palette = nav_mode ? NAV_MODE_PALETTE : PIANO_MODE_PALETTE;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, palette.normal);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette.hovered);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette.active);
|
||||
ImGui::Button(label, overlay::apply_scaling_to_vector(BUTTON_WIDTH, BUTTON_HEIGHT));
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
const auto &io = ImGui::GetIO();
|
||||
RECT client_rect {};
|
||||
if (io.DisplaySize.x > 0.f && io.DisplaySize.y > 0.f &&
|
||||
GetClientRect(this->overlay->get_window(), &client_rect)) {
|
||||
|
||||
// convert the rendered imgui rectangle into the client coordinates
|
||||
// used by hardware touch publication and piano-key mapping
|
||||
const auto item_min = ImGui::GetItemRectMin();
|
||||
const auto item_max = ImGui::GetItemRectMax();
|
||||
const auto client_width = client_rect.right - client_rect.left;
|
||||
const auto client_height = client_rect.bottom - client_rect.top;
|
||||
if (!nav_mode) {
|
||||
draw_piano_keys(
|
||||
io.DisplaySize,
|
||||
client_width,
|
||||
games::nost::touch_mode::piano_key_state());
|
||||
}
|
||||
|
||||
RECT button_bounds {
|
||||
static_cast<LONG>(std::lround(item_min.x * client_width / io.DisplaySize.x)),
|
||||
static_cast<LONG>(std::lround(item_min.y * client_height / io.DisplaySize.y)),
|
||||
static_cast<LONG>(std::lround(item_max.x * client_width / io.DisplaySize.x)),
|
||||
static_cast<LONG>(std::lround(item_max.y * client_height / io.DisplaySize.y)),
|
||||
};
|
||||
games::nost::touch_mode::publish_button_bounds(
|
||||
this->overlay->get_window(), button_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "overlay/window.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// persistent mode control rendered independently of the main overlay visibility
|
||||
class NostalgiaTouchPiano : public Window {
|
||||
public:
|
||||
explicit NostalgiaTouchPiano(SpiceOverlay *overlay);
|
||||
|
||||
void calculate_initial_window() override;
|
||||
void build_content() override;
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "overlay/window.h"
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "patcher/patch_manager.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
enum class PatchType {
|
||||
Unknown,
|
||||
Memory,
|
||||
Signature,
|
||||
Union,
|
||||
Integer,
|
||||
};
|
||||
|
||||
enum class PatchStatus {
|
||||
Error,
|
||||
Disabled,
|
||||
Enabled,
|
||||
};
|
||||
|
||||
enum class PatchUrlStatus {
|
||||
Valid,
|
||||
Invalid,
|
||||
Unapplied,
|
||||
ValidButNoData,
|
||||
Partial,
|
||||
};
|
||||
|
||||
struct MemoryPatch {
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
|
||||
size_t data_disabled_len = 0;
|
||||
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
|
||||
size_t data_enabled_len = 0;
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t *data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchData;
|
||||
struct SignaturePatch {
|
||||
std::string dll_name = "";
|
||||
std::string signature = "", replacement = "";
|
||||
uint64_t offset = 0;
|
||||
int64_t usage = 0;
|
||||
|
||||
MemoryPatch to_memory(PatchData *patch);
|
||||
};
|
||||
|
||||
struct UnionPatch {
|
||||
std::string name = "";
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data = nullptr;
|
||||
size_t data_len = 0;
|
||||
uint64_t offset = 0;
|
||||
uint8_t* data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct NumberPatch {
|
||||
std::string dll_name = "";
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t* data_offset_ptr = nullptr;
|
||||
int32_t min;
|
||||
int32_t max;
|
||||
int32_t value;
|
||||
size_t size_in_bytes;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchData {
|
||||
bool enabled;
|
||||
std::string game_code;
|
||||
int datecode_min = 0;
|
||||
int datecode_max = 0;
|
||||
std::string name, description, caution;
|
||||
std::string name_in_lower_case = "";
|
||||
PatchType type;
|
||||
bool preset;
|
||||
std::vector<MemoryPatch> patches_memory;
|
||||
std::vector<UnionPatch> patches_union;
|
||||
NumberPatch patch_number;
|
||||
PatchStatus last_status;
|
||||
std::string hash;
|
||||
bool unverified = false;
|
||||
std::string peIdentifier;
|
||||
std::string error_reason = "";
|
||||
|
||||
// for union patch only
|
||||
std::string selected_union_name = "";
|
||||
};
|
||||
|
||||
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||
|
||||
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info=false);
|
||||
|
||||
class PatchManager : public Window {
|
||||
public:
|
||||
|
||||
PatchManager(SpiceOverlay *overlay, bool apply_patches = false);
|
||||
explicit PatchManager(SpiceOverlay *overlay);
|
||||
~PatchManager() override;
|
||||
|
||||
void build_content() override;
|
||||
void reload_local_patches(bool apply_patches = false);
|
||||
bool import_remote_patches_to_disk();
|
||||
bool load_from_patches_json(bool apply_patches);
|
||||
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
||||
void hard_apply_patches();
|
||||
void load_embedded_patches(bool apply_patches);
|
||||
|
||||
private:
|
||||
|
||||
// configuration
|
||||
static std::filesystem::path config_path;
|
||||
static bool config_dirty;
|
||||
static bool setting_auto_apply;
|
||||
static std::vector<std::string> setting_auto_apply_list;
|
||||
static std::vector<std::string> setting_patches_enabled;
|
||||
static std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
static std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
static std::string patch_url;
|
||||
static std::string patch_name_filter;
|
||||
|
||||
static std::filesystem::path LOCAL_PATCHES_PATH;
|
||||
static std::string ACTIVE_JSON_FILE;
|
||||
|
||||
// patches
|
||||
static std::vector<PatchData> patches;
|
||||
static bool local_patches_initialized;
|
||||
|
||||
// cached sorted view of `patches` for the table, stored as indices so a
|
||||
// stale entry can never dangle if `patches` is rebuilt; rebuilt only when
|
||||
// the sort order changes or the patch list is reloaded (not every frame)
|
||||
static std::vector<size_t> patches_sorted;
|
||||
|
||||
void config_load();
|
||||
void config_save();
|
||||
|
||||
// rebuild the cached sorted view of `patches` from the active table's
|
||||
// sort specs; no-op unless the sort changed or the patch list changed
|
||||
void update_sorted_patches();
|
||||
|
||||
void append_patches(
|
||||
std::string &patches_json,
|
||||
bool apply_patches = false,
|
||||
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
||||
std::string pe_identifier_for_patch = "");
|
||||
|
||||
void show_patch_tooltip(const PatchData& patch);
|
||||
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
||||
void show_patch_tooltip(const patcher::PatchData& patch);
|
||||
};
|
||||
|
||||
PatchStatus is_patch_active(PatchData &patch);
|
||||
bool apply_patch(PatchData &patch, bool active);
|
||||
|
||||
int64_t parse_little_endian_int(uint8_t* bytes, size_t size);
|
||||
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size);
|
||||
|
||||
std::vector<uint8_t>* find_in_dll_map(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
std::vector<uint8_t>* find_in_dll_map_org(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
bool restore_bytes_from_dll_map_org(
|
||||
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
void create_dll_backup(
|
||||
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
||||
std::string fix_up_dll_name(const std::string& dll_name);
|
||||
uint8_t* get_dll_offset_for_patch_apply(
|
||||
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes);
|
||||
|
||||
uint64_t parse_json_data_offset(
|
||||
const std::string &patch_name, const rapidjson::Value &value);
|
||||
|
||||
void print_auto_apply_status(PatchData &patch);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "external/hash-library/sha256.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "external/rapidjson/prettywriter.h"
|
||||
#include "external/rapidjson/stringbuffer.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace patcher {
|
||||
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config) {
|
||||
return ((id_from_config.compare(0, 3, avs::game::MODEL) == 0) &&
|
||||
(id_from_config.compare(10, 10, avs::game::EXT) == 0));
|
||||
}
|
||||
|
||||
void config_load() {
|
||||
log_info("patchmanager", "loading config");
|
||||
|
||||
// read config file
|
||||
std::string config = fileutils::text_read(config_path);
|
||||
if (!config.empty()) {
|
||||
|
||||
// parse document
|
||||
Document doc;
|
||||
doc.Parse(config.c_str());
|
||||
|
||||
// check parse error
|
||||
auto error = doc.GetParseError();
|
||||
if (error) {
|
||||
log_warning("patchmanager", "config file parse error: {}", static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
// verify root is a dict
|
||||
if (doc.IsObject()) {
|
||||
|
||||
// read auto apply settings
|
||||
auto auto_apply = doc.FindMember("auto_apply");
|
||||
if (auto_apply != doc.MemberEnd() && auto_apply->value.IsArray()) {
|
||||
|
||||
// get game id
|
||||
const auto game_id = avs::game::get_identifier();
|
||||
|
||||
// iterate entries
|
||||
setting_auto_apply = false;
|
||||
setting_auto_apply_list.clear();
|
||||
for (auto &entry : auto_apply->value.GetArray()) {
|
||||
if (entry.IsString()) {
|
||||
|
||||
// check if this is our game identifier
|
||||
const std::string entry_id = entry.GetString();
|
||||
|
||||
if (!setting_auto_apply) {
|
||||
if (game_id == entry_id) {
|
||||
// exact match
|
||||
setting_auto_apply = true;
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"matched auto apply entry by full game identifier: {}",
|
||||
entry_id);
|
||||
|
||||
} else if (is_game_id_wildcard_matched(entry_id)) {
|
||||
// match on model and ext, ignoring dest/spec/rev
|
||||
// sample: LDJ:J:E:A:2025011400
|
||||
setting_auto_apply = true;
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"matched auto apply entry by partial game identifier: {}:?:?:?:{}",
|
||||
avs::game::MODEL, avs::game::EXT);
|
||||
}
|
||||
}
|
||||
|
||||
// move to list
|
||||
setting_auto_apply_list.emplace_back(entry_id);
|
||||
}
|
||||
}
|
||||
if (!setting_auto_apply) {
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"no auto apply entry matched, patches will not load automatically");
|
||||
}
|
||||
}
|
||||
|
||||
// read enabled patches
|
||||
auto patches_enabled = doc.FindMember("patches_enabled");
|
||||
if (patches_enabled != doc.MemberEnd() && patches_enabled->value.IsArray()) {
|
||||
setting_patches_enabled.clear();
|
||||
for (const auto &patch : patches_enabled->value.GetArray()) {
|
||||
if (patch.IsString()) {
|
||||
setting_patches_enabled.emplace_back(std::string(patch.GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// read enabled union patches
|
||||
auto patches_union_enabled = doc.FindMember("union_patches_enabled");
|
||||
if (patches_union_enabled != doc.MemberEnd() && patches_union_enabled->value.IsObject()) {
|
||||
setting_union_patches_enabled.clear();
|
||||
for (auto it = patches_union_enabled->value.MemberBegin(); it != patches_union_enabled->value.MemberEnd(); ++it) {
|
||||
if (it->name.IsString() && it->value.IsString()) {
|
||||
setting_union_patches_enabled[it->name.GetString()] = it->value.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
// read enabled integer patches
|
||||
auto patches_int_enabled = doc.FindMember("integer_patches_enabled");
|
||||
if (patches_int_enabled != doc.MemberEnd() && patches_int_enabled->value.IsObject()) {
|
||||
setting_int_patches_enabled.clear();
|
||||
for (auto it = patches_int_enabled->value.MemberBegin(); it != patches_int_enabled->value.MemberEnd(); ++it) {
|
||||
if (it->name.IsString() && it->value.IsNumber()) {
|
||||
setting_int_patches_enabled[it->name.GetString()] = it->value.GetInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read remote patch URLs
|
||||
auto remote_url_history = doc.FindMember("remote_url_history");
|
||||
if (remote_url_history != doc.MemberEnd() && remote_url_history->value.IsArray()) {
|
||||
url_recents.clear();
|
||||
for (const auto &url : remote_url_history->value.GetArray()) {
|
||||
if (url.IsString()) {
|
||||
url_recents.emplace_back(std::string(url.GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string patch_hash(PatchData &patch) {
|
||||
SHA256 hash;
|
||||
hash.add(patch.game_code.c_str(), patch.game_code.length());
|
||||
if (patch.datecode_min != 0 || patch.datecode_max != 0) {
|
||||
hash.add(&patch.datecode_min, sizeof(patch.datecode_min));
|
||||
hash.add(&patch.datecode_max, sizeof(patch.datecode_max));
|
||||
}
|
||||
if (!patch.peIdentifier.empty()) {
|
||||
hash.add(patch.peIdentifier.c_str(), patch.peIdentifier.length());
|
||||
}
|
||||
hash.add(patch.name.c_str(), patch.name.length());
|
||||
hash.add(patch.description.c_str(), patch.description.length());
|
||||
return hash.getHash();
|
||||
}
|
||||
|
||||
void config_save() {
|
||||
|
||||
// create document
|
||||
Document doc;
|
||||
doc.Parse(
|
||||
"{"
|
||||
" \"auto_apply\": [],"
|
||||
" \"patches_enabled\": [],"
|
||||
" \"union_patches_enabled\": {},"
|
||||
" \"integer_patches_enabled\": {},"
|
||||
" \"remote_url_history\": []"
|
||||
"}"
|
||||
);
|
||||
|
||||
// check parse error
|
||||
auto error = doc.GetParseError();
|
||||
if (error) {
|
||||
log_warning("patchmanager", "template parse error: {}", static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
// auto apply setting
|
||||
auto &auto_apply_list = doc["auto_apply"];
|
||||
auto game_id = avs::game::get_identifier();
|
||||
bool game_id_added = false;
|
||||
for (auto &entry : setting_auto_apply_list) {
|
||||
if (entry == game_id || is_game_id_wildcard_matched(entry)) {
|
||||
if (!setting_auto_apply) {
|
||||
continue;
|
||||
}
|
||||
game_id_added = true;
|
||||
}
|
||||
auto_apply_list.PushBack(StringRef(entry.c_str()), doc.GetAllocator());
|
||||
}
|
||||
if (setting_auto_apply && !game_id_added) {
|
||||
auto_apply_list.PushBack(StringRef(game_id.c_str()), doc.GetAllocator());
|
||||
}
|
||||
|
||||
// get enabled patches
|
||||
auto &doc_patches_enabled = doc["patches_enabled"];
|
||||
auto &doc_union_patches_enable = doc["union_patches_enabled"];
|
||||
auto &doc_int_patches_enable = doc["integer_patches_enabled"];
|
||||
for (auto &patch : patches) {
|
||||
auto hash = patch_hash(patch);
|
||||
|
||||
if (patch.type == PatchType::Union) {
|
||||
// enable hash if known as enabled, overridden and missing from list
|
||||
if (patch.enabled) {
|
||||
setting_union_patches_enabled[hash] = patch.selected_union_name;
|
||||
} else {
|
||||
setting_union_patches_enabled.erase(hash);
|
||||
}
|
||||
} else if (patch.type == PatchType::Integer) {
|
||||
if (patch.enabled) {
|
||||
setting_int_patches_enabled[hash] = patch.patch_number.value;
|
||||
} else {
|
||||
setting_int_patches_enabled.erase(hash);
|
||||
}
|
||||
} else {
|
||||
// hash patch and find entry
|
||||
auto entry = std::find(setting_patches_enabled.begin(), setting_patches_enabled.end(), hash);
|
||||
|
||||
// enable hash if known as enabled, overridden and missing from list
|
||||
if ((patch.last_status == PatchStatus::Enabled && patch.enabled)
|
||||
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && patch.enabled)) {
|
||||
if (entry == setting_patches_enabled.end()) {
|
||||
setting_patches_enabled.emplace_back(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// disable hash if patch known as disabled
|
||||
if (patch.last_status == PatchStatus::Disabled
|
||||
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && !patch.enabled)) {
|
||||
if (entry != setting_patches_enabled.end()) {
|
||||
setting_patches_enabled.erase(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add hashes to document
|
||||
for (auto &hash : setting_patches_enabled) {
|
||||
Value hash_value(hash.c_str(), doc.GetAllocator());
|
||||
doc_patches_enabled.PushBack(hash_value, doc.GetAllocator());
|
||||
}
|
||||
|
||||
for (auto& it : setting_union_patches_enabled) {
|
||||
const std::string& key = it.first;
|
||||
const std::string& val = it.second;
|
||||
doc_union_patches_enable.AddMember(StringRef(key.c_str()), StringRef(val.c_str()), doc.GetAllocator());
|
||||
}
|
||||
|
||||
for (auto& it : setting_int_patches_enabled) {
|
||||
const std::string& key = it.first;
|
||||
const int32_t& val = it.second;
|
||||
doc_int_patches_enable.AddMember(StringRef(key.c_str()), val, doc.GetAllocator());
|
||||
}
|
||||
|
||||
// remote URLs
|
||||
auto &doc_url_history = doc["remote_url_history"];
|
||||
for (auto& url : url_recents) {
|
||||
Value url_value(url.c_str(), doc.GetAllocator());
|
||||
doc_url_history.PushBack(url_value, doc.GetAllocator());
|
||||
}
|
||||
|
||||
// build JSON
|
||||
StringBuffer buffer;
|
||||
PrettyWriter<StringBuffer> writer(buffer);
|
||||
doc.Accept(writer);
|
||||
|
||||
// save to file
|
||||
if (fileutils::write_config_file("patchmanager", config_path, buffer.GetString())) {
|
||||
config_dirty = false;
|
||||
} else {
|
||||
log_warning("patchmanager", "unable to save config file");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "patch_manager.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/nt_loader.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
// internal engine state (patcher module only)
|
||||
extern std::vector<std::string> setting_auto_apply_list;
|
||||
extern std::vector<std::string> setting_patches_enabled;
|
||||
extern std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
extern std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
extern std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
extern std::filesystem::path LOCAL_PATCHES_PATH;
|
||||
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
||||
extern bool ldr_registered;
|
||||
extern void *ldr_notify_cookie;
|
||||
extern std::vector<std::string> ldr_target_libraries;
|
||||
|
||||
// internal helpers
|
||||
void append_patches(
|
||||
std::string& patches_json,
|
||||
bool apply_patches = false,
|
||||
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
||||
std::string pe_identifier_for_patch = "");
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
||||
std::string getFromUrl(const std::string& dll_name, const std::string& url);
|
||||
bool load_from_patches_json(bool apply_patches);
|
||||
void load_embedded_patches(bool apply_patches);
|
||||
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
||||
bool is_patch_group_definition(const rapidjson::Value& patch);
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
|
||||
const rapidjson::Document& doc);
|
||||
std::string resolve_patch_group_id(
|
||||
const rapidjson::Value& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const char *patch_name);
|
||||
void register_patch_group(
|
||||
PatchData& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions);
|
||||
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
|
||||
|
||||
std::string patch_hash(PatchData& patch);
|
||||
void clear_dll_maps();
|
||||
|
||||
int64_t parse_little_endian_int(uint8_t *bytes, size_t size);
|
||||
void int_to_little_endian_bytes(int64_t value, uint8_t *bytes, size_t size);
|
||||
|
||||
std::vector<uint8_t> *find_in_dll_map(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
std::vector<uint8_t> *find_in_dll_map_org(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
bool restore_bytes_from_dll_map_org(
|
||||
uint8_t *destination, const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
void create_dll_backup(
|
||||
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
||||
std::string fix_up_dll_name(const std::string& dll_name);
|
||||
uint8_t *get_dll_offset_for_patch_apply(
|
||||
const std::string& dll_name, uint64_t data_offset, size_t size_in_bytes);
|
||||
|
||||
uint64_t parse_json_data_offset(
|
||||
const std::string& patch_name, const rapidjson::Value& value);
|
||||
|
||||
void print_auto_apply_status(PatchData& patch);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||
|
||||
std::filesystem::path config_path;
|
||||
bool config_dirty = false;
|
||||
bool setting_auto_apply = false;
|
||||
|
||||
std::vector<std::string> setting_auto_apply_list;
|
||||
std::vector<std::string> setting_patches_enabled;
|
||||
|
||||
std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
|
||||
std::string patch_url;
|
||||
|
||||
std::filesystem::path LOCAL_PATCHES_PATH("patches");
|
||||
std::string ACTIVE_JSON_FILE;
|
||||
|
||||
std::vector<PatchData> patches;
|
||||
bool local_patches_initialized = false;
|
||||
std::vector<size_t> patches_sorted;
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
|
||||
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
||||
{"arkmdxp3.dll", {"gamemdx.dll"}},
|
||||
{"arkmdxp4.dll", {"gamemdx.dll"}},
|
||||
{"arkmdxbio2.dll", {"gamemdx.dll"}},
|
||||
{"arkndd.dll", {"gamendd.dll"}},
|
||||
{"arkkep.dll", {"game.dll"}},
|
||||
{"arkjc9.dll", {"gamejc9.dll"}},
|
||||
{"arkkdm.dll", {"gamekdm.dll"}},
|
||||
{"arkmmd.dll", {"gamemmd.dll"}},
|
||||
{"arkklp.dll", {"lpac.dll"}},
|
||||
{"arknck.dll", {"weac.dll"}},
|
||||
{"gdxg.dll", {"game.dll", "libshare-pj.dll", "boot.dll"}}
|
||||
};
|
||||
|
||||
std::string url_fetch_errors;
|
||||
size_t url_recent_idx = -1;
|
||||
std::vector<std::string> url_recents;
|
||||
|
||||
bool ldr_registered = false;
|
||||
void *ldr_notify_cookie = nullptr;
|
||||
std::vector<std::string> ldr_target_libraries;
|
||||
|
||||
void init() {
|
||||
if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) {
|
||||
config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value();
|
||||
log_info("patchmanager", "using custom config file path: {}", config_path);
|
||||
} else {
|
||||
config_path =
|
||||
fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json");
|
||||
}
|
||||
|
||||
// register for DLL load notifications so patches can be (re)applied as
|
||||
// the game's target libraries come into memory. registration happens
|
||||
// once and is left in place for the lifetime of the process.
|
||||
if (!ldr_registered) {
|
||||
ldr_target_libraries = getExtraDlls(avs::game::DLL_NAME);
|
||||
ldr_target_libraries.push_back(avs::game::DLL_NAME);
|
||||
|
||||
const auto register_fn = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
|
||||
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
|
||||
|
||||
if (register_fn && NT_SUCCESS(register_fn(
|
||||
0, &loader_notification, nullptr, &ldr_notify_cookie))) {
|
||||
log_info("patchmanager", "registered for DLL load notifications");
|
||||
} else {
|
||||
log_warning("patchmanager", "failed to register for DLL load notifications");
|
||||
}
|
||||
|
||||
ldr_registered = true;
|
||||
}
|
||||
|
||||
if (!local_patches_initialized) {
|
||||
patch_url.clear();
|
||||
if (fileutils::file_exists(config_path)) {
|
||||
config_load();
|
||||
}
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_patches_on_start() {
|
||||
if (!local_patches_initialized) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
|
||||
VOID CALLBACK loader_notification(
|
||||
ULONG reason,
|
||||
PCLDR_DLL_NOTIFICATION_DATA data,
|
||||
PVOID) {
|
||||
|
||||
if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
|
||||
const auto dll = strtolower(std::filesystem::path({
|
||||
data->Loaded.FullDllName->Buffer,
|
||||
data->Loaded.FullDllName->Length / sizeof(wchar_t)
|
||||
}).filename().string());
|
||||
|
||||
if (std::ranges::find(ldr_target_libraries, dll) != ldr_target_libraries.end()) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <cstring>
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
static std::pair<std::string, std::string> make_patch_group_key(
|
||||
const std::string& game_code,
|
||||
const std::string& group_id) {
|
||||
return {strtolower(game_code), group_id};
|
||||
}
|
||||
|
||||
static bool has_embedded_null(const rapidjson::Value& value) {
|
||||
return strlen(value.GetString()) != value.GetStringLength();
|
||||
}
|
||||
|
||||
bool is_patch_group_definition(const rapidjson::Value& patch) {
|
||||
if (!patch.IsObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto type_it = patch.FindMember("type");
|
||||
return type_it != patch.MemberEnd()
|
||||
&& type_it->value.IsString()
|
||||
&& type_it->value.GetStringLength() == strlen("group")
|
||||
&& !_stricmp(type_it->value.GetString(), "group");
|
||||
}
|
||||
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
|
||||
const rapidjson::Document& doc) {
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> groups;
|
||||
|
||||
for (const auto& patch : doc.GetArray()) {
|
||||
if (!is_patch_group_definition(patch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto id_it = patch.FindMember("id");
|
||||
const auto game_code_it = patch.FindMember("gameCode");
|
||||
const auto name_it = patch.FindMember("name");
|
||||
if (id_it == patch.MemberEnd() || !id_it->value.IsString()
|
||||
|| id_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(id_it->value)
|
||||
|| game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()
|
||||
|| game_code_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(game_code_it->value)
|
||||
|| name_it == patch.MemberEnd() || !name_it->value.IsString()
|
||||
|| name_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(name_it->value)) {
|
||||
log_warning("patchmanager", "invalid patch group definition");
|
||||
continue;
|
||||
}
|
||||
|
||||
PatchGroup group;
|
||||
group.name.assign(name_it->value.GetString(), name_it->value.GetStringLength());
|
||||
group.name_in_lower_case = strtolower(group.name);
|
||||
|
||||
const std::string group_id(
|
||||
id_it->value.GetString(),
|
||||
id_it->value.GetStringLength());
|
||||
|
||||
const auto description_it = patch.FindMember("description");
|
||||
if (description_it != patch.MemberEnd()) {
|
||||
if (!description_it->value.IsString()
|
||||
|| has_embedded_null(description_it->value)) {
|
||||
log_warning("patchmanager", "invalid description for patch group {}", group_id);
|
||||
continue;
|
||||
}
|
||||
group.description.assign(
|
||||
description_it->value.GetString(),
|
||||
description_it->value.GetStringLength());
|
||||
}
|
||||
|
||||
const auto caution_it = patch.FindMember("caution");
|
||||
if (caution_it != patch.MemberEnd()) {
|
||||
if (!caution_it->value.IsString() || has_embedded_null(caution_it->value)) {
|
||||
log_warning("patchmanager", "invalid caution for patch group {}", group_id);
|
||||
continue;
|
||||
}
|
||||
group.caution.assign(
|
||||
caution_it->value.GetString(),
|
||||
caution_it->value.GetStringLength());
|
||||
}
|
||||
|
||||
const std::string game_code(
|
||||
game_code_it->value.GetString(),
|
||||
game_code_it->value.GetStringLength());
|
||||
if (!groups.emplace(
|
||||
make_patch_group_key(game_code, group_id),
|
||||
std::move(group)).second) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"duplicate patch group definition for {}/{}, ignoring duplicate",
|
||||
game_code,
|
||||
group_id);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
static const PatchGroup* find_patch_group(
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const std::string& group_id) {
|
||||
const auto group = groups.find(make_patch_group_key(game_code, group_id));
|
||||
return group == groups.end() ? nullptr : &group->second;
|
||||
}
|
||||
|
||||
const PatchGroup* find_patch_group(const PatchData& patch) {
|
||||
return find_patch_group(patch_groups, patch.game_code, patch.group_id);
|
||||
}
|
||||
|
||||
std::string resolve_patch_group_id(
|
||||
const rapidjson::Value& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const char *patch_name) {
|
||||
const auto group_it = patch.FindMember("group");
|
||||
if (group_it == patch.MemberEnd()) {
|
||||
return "";
|
||||
}
|
||||
if (!group_it->value.IsString()
|
||||
|| group_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(group_it->value)) {
|
||||
log_warning("patchmanager", "invalid group reference for {}", patch_name);
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string group_id(
|
||||
group_it->value.GetString(),
|
||||
group_it->value.GetStringLength());
|
||||
if (!find_patch_group(groups, game_code, group_id)) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"unknown patch group {}/{} referenced by {}",
|
||||
game_code,
|
||||
group_id,
|
||||
patch_name);
|
||||
return "";
|
||||
}
|
||||
|
||||
return group_id;
|
||||
}
|
||||
|
||||
void register_patch_group(
|
||||
PatchData& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions) {
|
||||
if (patch.group_id.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto *definition = find_patch_group(
|
||||
definitions,
|
||||
patch.game_code,
|
||||
patch.group_id);
|
||||
if (!definition) {
|
||||
patch.group_id.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto key = make_patch_group_key(patch.game_code, patch.group_id);
|
||||
const auto [existing, inserted] = patch_groups.emplace(key, *definition);
|
||||
if (!inserted
|
||||
&& (existing->second.name != definition->name
|
||||
|| existing->second.description != definition->description
|
||||
|| existing->second.caution != definition->caution)) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"conflicting group metadata for {}/{}, ignoring group on {}",
|
||||
patch.game_code,
|
||||
patch.group_id,
|
||||
patch.name);
|
||||
patch.group_id.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace patcher {
|
||||
|
||||
enum class PatchType {
|
||||
Unknown,
|
||||
Memory,
|
||||
Signature,
|
||||
Union,
|
||||
Integer,
|
||||
};
|
||||
|
||||
enum class PatchStatus {
|
||||
Error,
|
||||
Disabled,
|
||||
Enabled,
|
||||
};
|
||||
|
||||
enum class PatchUrlStatus {
|
||||
Valid,
|
||||
Invalid,
|
||||
Unapplied,
|
||||
ValidButNoData,
|
||||
Partial,
|
||||
};
|
||||
|
||||
struct MemoryPatch {
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
|
||||
size_t data_disabled_len = 0;
|
||||
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
|
||||
size_t data_enabled_len = 0;
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t *data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchData;
|
||||
struct SignaturePatch {
|
||||
std::string dll_name = "";
|
||||
std::string signature = "", replacement = "";
|
||||
uint64_t offset = 0;
|
||||
int64_t usage = 0;
|
||||
|
||||
MemoryPatch to_memory(PatchData *patch);
|
||||
};
|
||||
|
||||
struct UnionPatch {
|
||||
std::string name = "";
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data = nullptr;
|
||||
size_t data_len = 0;
|
||||
uint64_t offset = 0;
|
||||
uint8_t *data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct NumberPatch {
|
||||
std::string dll_name = "";
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t *data_offset_ptr = nullptr;
|
||||
int32_t min;
|
||||
int32_t max;
|
||||
int32_t value;
|
||||
size_t size_in_bytes;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchGroup {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string caution;
|
||||
std::string name_in_lower_case;
|
||||
};
|
||||
|
||||
struct PatchData {
|
||||
bool enabled;
|
||||
std::string game_code;
|
||||
int datecode_min = 0;
|
||||
int datecode_max = 0;
|
||||
std::string name, description, caution;
|
||||
std::string name_in_lower_case = "";
|
||||
PatchType type;
|
||||
bool preset;
|
||||
std::vector<MemoryPatch> patches_memory;
|
||||
std::vector<UnionPatch> patches_union;
|
||||
NumberPatch patch_number;
|
||||
std::string group_id;
|
||||
PatchStatus last_status;
|
||||
std::string hash;
|
||||
bool unverified = false;
|
||||
std::string peIdentifier;
|
||||
std::string error_reason = "";
|
||||
|
||||
// for union patch only
|
||||
std::string selected_union_name = "";
|
||||
};
|
||||
|
||||
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||
|
||||
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info = false);
|
||||
|
||||
// lifecycle: init() sets up the config path and registers for DLL-load
|
||||
// notifications; apply_patches_on_start() applies auto-apply patches once the
|
||||
// game DLL is loaded. both are safe to call repeatedly.
|
||||
void init();
|
||||
void apply_patches_on_start();
|
||||
|
||||
// patch load / apply operations (also driven by the overlay window)
|
||||
void reload_local_patches(bool apply_patches = false);
|
||||
bool import_remote_patches_to_disk();
|
||||
void hard_apply_patches();
|
||||
void config_load();
|
||||
void config_save();
|
||||
std::vector<std::string> getExtraDlls(const std::string& firstDll);
|
||||
|
||||
// process-wide patch engine state, rendered and edited by the overlay window
|
||||
extern std::filesystem::path config_path;
|
||||
extern bool config_dirty;
|
||||
extern bool setting_auto_apply;
|
||||
extern std::string patch_url;
|
||||
extern std::string ACTIVE_JSON_FILE;
|
||||
extern std::vector<PatchData> patches;
|
||||
extern bool local_patches_initialized;
|
||||
extern std::vector<size_t> patches_sorted;
|
||||
extern std::string url_fetch_errors;
|
||||
extern size_t url_recent_idx;
|
||||
extern std::vector<std::string> url_recents;
|
||||
|
||||
PatchStatus is_patch_active(PatchData& patch);
|
||||
bool apply_patch(PatchData& patch, bool active);
|
||||
const PatchGroup* find_patch_group(const PatchData& patch);
|
||||
|
||||
std::string displayPath(const std::filesystem::path& path);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -197,8 +197,8 @@ namespace rawinput {
|
||||
HANDLE handle = INVALID_HANDLE_VALUE;
|
||||
DeviceType type = UNKNOWN;
|
||||
DeviceInfo info;
|
||||
std::mutex *mutex;
|
||||
std::mutex *mutex_out;
|
||||
std::mutex *mutex = nullptr;
|
||||
std::mutex *mutex_out = nullptr;
|
||||
bool updated = true;
|
||||
bool output_pending = true;
|
||||
bool output_enabled = false;
|
||||
@@ -213,5 +213,7 @@ namespace rawinput {
|
||||
double input_time = 0.0;
|
||||
double input_hz = 0.f;
|
||||
double input_hz_max = 0.f;
|
||||
|
||||
// when adding or removing a field, update replace_device_slot to match
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
#include <dbt.h>
|
||||
#include <devguid.h>
|
||||
#include <hidclass.h>
|
||||
#include <ntddkbd.h>
|
||||
#include <ntddmou.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput.h"
|
||||
@@ -12,31 +16,42 @@
|
||||
|
||||
namespace rawinput {
|
||||
|
||||
DEFINE_GUID(GUID_HID, 0x4D1E55B2L, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30);
|
||||
static bool is_rawinput_interface(const GUID &guid) {
|
||||
return IsEqualGUID(guid, GUID_DEVINTERFACE_HID)
|
||||
|| IsEqualGUID(guid, GUID_DEVINTERFACE_KEYBOARD)
|
||||
|| IsEqualGUID(guid, GUID_DEVINTERFACE_MOUSE);
|
||||
}
|
||||
|
||||
HotplugManager::HotplugManager(RawInputManager *ri_mgr, HWND hWnd) : ri_mgr(ri_mgr) {
|
||||
auto register_notification = [hWnd](const GUID &guid, const char *name) -> HANDLE {
|
||||
DEV_BROADCAST_DEVICEINTERFACE settings {
|
||||
.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE),
|
||||
.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
|
||||
.dbcc_reserved = 0,
|
||||
.dbcc_classguid = guid,
|
||||
.dbcc_name = {0},
|
||||
};
|
||||
|
||||
// init settings
|
||||
DEV_BROADCAST_DEVICEINTERFACE settings_hid {
|
||||
.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE),
|
||||
.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
|
||||
.dbcc_reserved = 0,
|
||||
.dbcc_classguid = GUID_HID,
|
||||
.dbcc_name = {0},
|
||||
auto notification = RegisterDeviceNotification(hWnd, &settings, DEVICE_NOTIFY_WINDOW_HANDLE);
|
||||
if (notification == nullptr) {
|
||||
log_warning("hotplug", "failed to register {} notifications: {}", name, GetLastError());
|
||||
}
|
||||
return notification;
|
||||
};
|
||||
|
||||
// register notifications
|
||||
this->hotplug_hid = RegisterDeviceNotification(hWnd, &settings_hid, DEVICE_NOTIFY_WINDOW_HANDLE);
|
||||
if (this->hotplug_hid == nullptr) {
|
||||
log_warning("hotplug", "failed to register HID notifications: {}", GetLastError());
|
||||
}
|
||||
this->hotplug_hid = register_notification(GUID_DEVINTERFACE_HID, "HID");
|
||||
this->hotplug_keyboard = register_notification(GUID_DEVINTERFACE_KEYBOARD, "keyboard");
|
||||
this->hotplug_mouse = register_notification(GUID_DEVINTERFACE_MOUSE, "mouse");
|
||||
}
|
||||
|
||||
HotplugManager::~HotplugManager() {
|
||||
|
||||
// unregister notifications
|
||||
if (this->hotplug_hid != nullptr) {
|
||||
UnregisterDeviceNotification(this->hotplug_hid);
|
||||
for (auto notification : {this->hotplug_hid, this->hotplug_keyboard, this->hotplug_mouse}) {
|
||||
if (notification != nullptr) {
|
||||
UnregisterDeviceNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +69,16 @@ namespace rawinput {
|
||||
switch (hdr->dbch_devicetype) {
|
||||
case DBT_DEVTYP_DEVICEINTERFACE: {
|
||||
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
|
||||
this->ri_mgr->devices_scan_midi();
|
||||
|
||||
// check if class is not HID
|
||||
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
|
||||
// check if this is one of the registered RawInput interfaces
|
||||
if (!is_rawinput_interface(dev->dbcc_classguid)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// keyboard and mouse class events accompany the HID event, so scan MIDI only once
|
||||
if (IsEqualGUID(dev->dbcc_classguid, GUID_DEVINTERFACE_HID)) {
|
||||
this->ri_mgr->midi_scan_start();
|
||||
}
|
||||
|
||||
// hotplug
|
||||
std::string name(dev->dbcc_name);
|
||||
@@ -109,9 +129,13 @@ namespace rawinput {
|
||||
switch (hdr->dbch_devicetype) {
|
||||
case DBT_DEVTYP_DEVICEINTERFACE: {
|
||||
auto dev = (DEV_BROADCAST_DEVICEINTERFACE*) hdr;
|
||||
if (!is_rawinput_interface(dev->dbcc_classguid)) {
|
||||
break;
|
||||
}
|
||||
std::string name(dev->dbcc_name);
|
||||
|
||||
// destruct device
|
||||
log_misc("hotplug", "device interface removal: {}", name);
|
||||
this->ri_mgr->devices_remove(name);
|
||||
}
|
||||
}
|
||||
@@ -120,8 +144,11 @@ namespace rawinput {
|
||||
return TRUE;
|
||||
}
|
||||
case DBT_DEVNODES_CHANGED: {
|
||||
// TODO: this can be a little noisy as it gets called on every device
|
||||
this->ri_mgr->devices_scan_midi();
|
||||
// catch-all device-tree change: MIDI and XInput have no targeted
|
||||
// arrival/removal notification, so this is our only hook to detect them
|
||||
// can be a little noisy as it gets called on every device
|
||||
log_misc("hotplug", "device tree changed, rescanning MIDI + XInput");
|
||||
this->ri_mgr->midi_scan_start();
|
||||
this->ri_mgr->devices_scan_xinput();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ namespace rawinput {
|
||||
class HotplugManager {
|
||||
private:
|
||||
RawInputManager *ri_mgr;
|
||||
HANDLE hotplug_hid;
|
||||
HANDLE hotplug_hid = nullptr;
|
||||
HANDLE hotplug_keyboard = nullptr;
|
||||
HANDLE hotplug_mouse = nullptr;
|
||||
|
||||
public:
|
||||
HotplugManager(RawInputManager *ri_mgr, HWND hwnd);
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
#include "rawinput.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace rawinput {
|
||||
|
||||
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
||||
}
|
||||
|
||||
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
||||
return rawinput::MIDI_NOTE_ALGORITHM;
|
||||
}
|
||||
|
||||
void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
|
||||
rawinput::MIDI_NOTE_ALGORITHM = new_algo;
|
||||
std::string s = "Unknown";
|
||||
switch (new_algo) {
|
||||
case rawinput::MidiNoteAlgorithm::LEGACY:
|
||||
s = "legacy";
|
||||
break;
|
||||
case rawinput::MidiNoteAlgorithm::V2:
|
||||
s = "v2";
|
||||
break;
|
||||
case rawinput::MidiNoteAlgorithm::V2_DRUM:
|
||||
s = "v2_drum";
|
||||
break;
|
||||
default:
|
||||
log_info("rawinput", "assert failed: invalid midi algorithm");
|
||||
break;
|
||||
}
|
||||
log_info("rawinput", "using MIDI algorithm: {}", s);
|
||||
}
|
||||
|
||||
void rawinput::RawInputManager::midi_scan_start() {
|
||||
|
||||
// single-flight: only one scan runs at a time. if one is already running, set
|
||||
// the pending flag so it rescans once more when it finishes - MIDI hotplug
|
||||
// events fire while the slow enumeration is still going and must not be lost.
|
||||
// the scheduler mutex makes this check-and-set atomic with the worker's
|
||||
// exit-or-rescan decision below, so a request set while a scan is running is
|
||||
// never dropped
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->midi_scan_m);
|
||||
if (this->midi_scan_active) {
|
||||
this->midi_scan_pending = true;
|
||||
log_misc("rawinput", "MIDI scan already running, queued rescan");
|
||||
return;
|
||||
}
|
||||
this->midi_scan_active = true;
|
||||
this->midi_scan_pending = false;
|
||||
}
|
||||
|
||||
// clean up the previous (already finished) scan thread handle
|
||||
this->midi_scan_join();
|
||||
|
||||
// run the (potentially slow) MIDI enumeration on its own thread so callers are
|
||||
// not blocked while the Windows MIDI subsystem starts up. rescan if a request
|
||||
// arrived while we were scanning
|
||||
log_misc("rawinput", "starting async MIDI scan thread");
|
||||
this->midi_thread = new std::thread([this]() {
|
||||
for (;;) {
|
||||
this->devices_scan_midi();
|
||||
|
||||
// decide whether to exit under the scheduler lock, atomically with any
|
||||
// concurrent midi_scan_start(): if a rescan was requested, consume it
|
||||
// and loop; otherwise clear active and exit. because both sides take
|
||||
// the same lock, a request set while active is true is never lost, so
|
||||
// we never strand a hotplug event waiting for a future one
|
||||
std::lock_guard<std::mutex> lock(this->midi_scan_m);
|
||||
if (!this->midi_scan_pending) {
|
||||
this->midi_scan_active = false;
|
||||
log_misc("rawinput", "async MIDI scan thread finished");
|
||||
return;
|
||||
}
|
||||
this->midi_scan_pending = false;
|
||||
log_misc("rawinput", "async MIDI scan rescanning (event arrived during scan)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void rawinput::RawInputManager::midi_scan_join() {
|
||||
if (this->midi_thread) {
|
||||
if (this->midi_thread->joinable()) {
|
||||
|
||||
// this blocks until the scan worker returns. if it ever hangs here the
|
||||
// worker is stuck - most likely in midi_close_deferred_flush() waiting
|
||||
// on a WinMM close. a missing "joined" line pinpoints the hang
|
||||
log_misc("rawinput", "joining MIDI scan thread...");
|
||||
this->midi_thread->join();
|
||||
log_misc("rawinput", "MIDI scan thread joined");
|
||||
}
|
||||
delete this->midi_thread;
|
||||
this->midi_thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void rawinput::RawInputManager::midi_close_deferred_flush() {
|
||||
|
||||
// take the queued handles under the lock, then close them without it. WinMM
|
||||
// midiInReset/midiInClose block until in-flight input_midi_proc callbacks
|
||||
// return, and those callbacks take devices_mutex, so closing under the lock
|
||||
// would deadlock
|
||||
std::vector<HMIDIIN> handles;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||
handles.swap(this->midi_close_deferred);
|
||||
}
|
||||
if (handles.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if a hang is ever reported here it is the classic WinMM deadlock: an
|
||||
// in-flight input_midi_proc callback is blocked on devices_mutex while
|
||||
// midiInReset/midiInClose waits for that callback to return. the per-handle
|
||||
// log below pinpoints exactly which close did not come back
|
||||
log_misc("rawinput", "closing {} deferred MIDI handle(s)", handles.size());
|
||||
for (size_t i = 0; i < handles.size(); i++) {
|
||||
log_misc("rawinput", "closing deferred MIDI handle {}/{}", i + 1, handles.size());
|
||||
midiInReset(handles[i]);
|
||||
midiInClose(handles[i]);
|
||||
}
|
||||
log_misc("rawinput", "deferred MIDI handles closed");
|
||||
}
|
||||
|
||||
void rawinput::RawInputManager::devices_scan_midi() {
|
||||
log_misc("rawinput", "scan MIDI devices...");
|
||||
|
||||
// note: the WinMM MIDI calls below (midiInGetNumDevs / midiInGetDevCaps /
|
||||
// midiInOpen / midiInStart) can block for seconds while the Windows MIDI
|
||||
// subsystem starts up, so they must NOT run under devices_mutex. only the
|
||||
// list mutation at the end of each iteration is guarded.
|
||||
|
||||
// identifiers of every MIDI device seen in this scan; used below to
|
||||
// tombstone devices that have since been unplugged
|
||||
std::vector<std::string> present_identifiers;
|
||||
|
||||
// add midi devices
|
||||
auto midi_device_count = midiInGetNumDevs();
|
||||
for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) {
|
||||
|
||||
// get dev caps
|
||||
MIDIINCAPS midi_device_caps{};
|
||||
if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}",
|
||||
midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid);
|
||||
|
||||
// build identifier for MIDI
|
||||
// ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file)
|
||||
// so it should never be changed
|
||||
std::ostringstream midi_identifier_stream;
|
||||
midi_identifier_stream << ";" << "MIDI";
|
||||
midi_identifier_stream << ";" << midi_device_id;
|
||||
midi_identifier_stream << ";" << midi_device_caps.szPname;
|
||||
midi_identifier_stream << ";" << midi_device_caps.wMid;
|
||||
midi_identifier_stream << ";" << midi_device_caps.wPid;
|
||||
const auto midi_identifier = midi_identifier_stream.str();
|
||||
|
||||
// record that this device is currently present
|
||||
present_identifiers.push_back(midi_identifier);
|
||||
|
||||
// if already open, leave it alone: hotplug fires many change events, and
|
||||
// reopening on every rescan would drop the WinMM handle (and its input).
|
||||
// only (re)open when the device is missing or a destroyed tombstone
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||
bool already_open = false;
|
||||
for (auto &device : this->devices) {
|
||||
if (device.type == MIDI && device.name == midi_identifier) {
|
||||
already_open = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (already_open) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// open device
|
||||
HMIDIIN midi_device_handle;
|
||||
if (midiInOpen(&midi_device_handle,
|
||||
(UINT) midi_device_id,
|
||||
(DWORD_PTR) &input_midi_proc,
|
||||
(DWORD_PTR) this,
|
||||
CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// start input
|
||||
if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) {
|
||||
|
||||
// close the handle we just opened so it does not leak on repeated rescans
|
||||
midiInClose(midi_device_handle);
|
||||
continue;
|
||||
}
|
||||
|
||||
// device info
|
||||
DeviceInfo midi_device_info {};
|
||||
|
||||
// device midi info
|
||||
auto midi_device_midi_info = new DeviceMIDIInfo();
|
||||
midi_device_midi_info->states = std::vector<bool>(16 * 128);
|
||||
midi_device_midi_info->states_events = std::vector<uint8_t>(16 * 128);
|
||||
midi_device_midi_info->bind_states = std::vector<bool>(16 * 128);
|
||||
midi_device_midi_info->v2_last_on_time = std::vector<double>(16 * 128);
|
||||
midi_device_midi_info->v2_last_off_time = std::vector<double>(16 * 128);
|
||||
midi_device_midi_info->v2_velocity_threshold = std::vector<uint8_t>(16 * 128);
|
||||
midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector<bool>(16 * 128);
|
||||
midi_device_midi_info->velocity = std::vector<uint8_t>(16 * 128);
|
||||
midi_device_midi_info->freeze = false;
|
||||
midi_device_midi_info->controls_precision = std::vector<uint16_t>(16 * 32);
|
||||
midi_device_midi_info->controls_precision_bind = std::vector<uint16_t>(16 * 32);
|
||||
midi_device_midi_info->controls_precision_msb = std::vector<bool>(16 * 32);
|
||||
midi_device_midi_info->controls_precision_lsb = std::vector<bool>(16 * 32);
|
||||
midi_device_midi_info->controls_precision_set = std::vector<bool>(16 * 32);
|
||||
midi_device_midi_info->controls_single = std::vector<uint8_t>(16 * 44);
|
||||
midi_device_midi_info->controls_single_bind = std::vector<uint8_t>(16 * 44);
|
||||
midi_device_midi_info->controls_single_set = std::vector<bool>(16 * 44);
|
||||
midi_device_midi_info->controls_onoff = std::vector<bool>(16 * 6);
|
||||
midi_device_midi_info->controls_onoff_bind = std::vector<bool>(16 * 6);
|
||||
midi_device_midi_info->controls_onoff_set = std::vector<bool>(16 * 6);
|
||||
midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector<double>(16 * 6);
|
||||
midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector<double>(16 * 6);
|
||||
midi_device_midi_info->pitch_bend = std::vector<int16_t>(16 * 6);
|
||||
midi_device_midi_info->pitch_bend_set = std::vector<bool>(16 * 6);
|
||||
|
||||
// build device
|
||||
Device midi_device {};
|
||||
midi_device.type = MIDI;
|
||||
midi_device.handle = midi_device_handle;
|
||||
midi_device.name = midi_identifier;
|
||||
midi_device.desc = to_string(midi_device_caps.szPname);
|
||||
midi_device.info = midi_device_info;
|
||||
midi_device.midiInfo = midi_device_midi_info;
|
||||
|
||||
// mutate the shared device list under lock (the slow WinMM calls above
|
||||
// ran without it so other threads were not blocked)
|
||||
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||
midi_device.id = devices.size() + 1;
|
||||
|
||||
// reuse a previously destroyed tombstone with the same identifier, if any.
|
||||
// (a live device with this identifier was already skipped above)
|
||||
bool replaced = false;
|
||||
for (auto &device : this->devices) {
|
||||
if (device.name == midi_identifier) {
|
||||
|
||||
// carry over ID
|
||||
midi_device.id = device.id;
|
||||
|
||||
// destruct and replace the slot in place under its locks
|
||||
this->devices_destruct(&device);
|
||||
replace_device_slot(device, midi_device);
|
||||
|
||||
// notify change
|
||||
for (auto &cb : this->callback_change) {
|
||||
cb.f(cb.data, &device);
|
||||
}
|
||||
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replaced) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// add device to list
|
||||
midi_device.mutex = new std::mutex();
|
||||
midi_device.mutex_out = new std::mutex();
|
||||
auto &device = this->devices.emplace_back(midi_device);
|
||||
|
||||
// notify add
|
||||
for (auto &cb : this->callback_add) {
|
||||
cb.f(cb.data, &device);
|
||||
}
|
||||
}
|
||||
|
||||
// tombstone MIDI devices that were open but are no longer present (unplugged).
|
||||
// otherwise a replugged device matches the stale live entry in the skip check
|
||||
// above and never gets reopened, silently losing its input
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||
for (auto &device : this->devices) {
|
||||
if (device.type != MIDI) {
|
||||
continue;
|
||||
}
|
||||
bool present = false;
|
||||
for (const auto &identifier : present_identifiers) {
|
||||
if (identifier == device.name) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!present) {
|
||||
log_info("rawinput", "MIDI device unplugged, releasing: {}", device.desc);
|
||||
this->devices_destruct(&device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close the MIDI handles detached above, now that devices_mutex is released
|
||||
this->midi_close_deferred_flush();
|
||||
|
||||
log_misc("rawinput", "scan MIDI devices done ({} enumerated)", (unsigned) midi_device_count);
|
||||
}
|
||||
|
||||
void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance,
|
||||
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
||||
// get instance
|
||||
auto ri_mgr = reinterpret_cast<RawInputManager *>(dwInstance);
|
||||
|
||||
// handle message
|
||||
switch (wMsg) {
|
||||
case MIM_OPEN:
|
||||
case MIM_CLOSE:
|
||||
break;
|
||||
case MIM_MOREDATA:
|
||||
case MIM_DATA: {
|
||||
|
||||
// lock the device list so a concurrent scan can't mutate it while we iterate
|
||||
std::lock_guard<std::recursive_mutex> devices_lock(ri_mgr->devices_mutex);
|
||||
|
||||
// param mapping
|
||||
auto dwMidiMessage = dwParam1;
|
||||
//auto dwTimestamp = dwParam2;
|
||||
|
||||
// message unpacking
|
||||
auto midi_status = LOBYTE(LOWORD(dwMidiMessage));
|
||||
auto midi_status_command = (midi_status & 0xF0u) >> 4u;
|
||||
auto midi_status_channel = (midi_status & 0x0Fu);
|
||||
auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage));
|
||||
auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage));
|
||||
|
||||
// callbacks
|
||||
for (auto &callback : ri_mgr->callback_midi) {
|
||||
|
||||
// find device
|
||||
for (auto &device : ri_mgr->devices_get()) {
|
||||
if (device.type == MIDI && device.handle == hMidiIn) {
|
||||
|
||||
// call function
|
||||
callback.f(callback.data, &device,
|
||||
midi_status_command, midi_status_channel,
|
||||
midi_byte1, midi_byte2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip unused messages types early for performance
|
||||
bool skip = false;
|
||||
switch (midi_status_command) {
|
||||
case 0xA: // POLYPHONIC PRESSURE
|
||||
case 0xC: // PROGRAM CHANGE
|
||||
case 0xD: // CHANNEL PRESSURE
|
||||
case 0xF: // SYSTEM EXCLUSIVE
|
||||
skip = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (skip) {
|
||||
break;
|
||||
}
|
||||
|
||||
// find device
|
||||
for (auto &device : ri_mgr->devices_get()) {
|
||||
|
||||
// filter non MIDI devices
|
||||
if (device.type != MIDI) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter wrong handles
|
||||
if (device.handle != hMidiIn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get input time
|
||||
const auto input_time = get_performance_seconds();
|
||||
|
||||
// lock device
|
||||
std::lock_guard<std::mutex> lock(*device.mutex);
|
||||
|
||||
// update hz
|
||||
auto diff_time = input_time - device.input_time;
|
||||
if (diff_time > 0.0001) {
|
||||
device.input_hz = 1.f / diff_time;
|
||||
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
|
||||
device.input_time = input_time;
|
||||
}
|
||||
|
||||
// command logic
|
||||
switch (midi_status_command) {
|
||||
case 0x8: { // NOTE OFF
|
||||
|
||||
// param mapping
|
||||
const auto midi_note = midi_byte1 & 127u;
|
||||
|
||||
// log_misc("midi", "[{}] OFF", midi_note);
|
||||
|
||||
// get index
|
||||
const auto midi_index = midi_status_channel * 128 + midi_note;
|
||||
if (midi_index < 16 * 128) {
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||
// update velocity
|
||||
device.midiInfo->velocity[midi_index] = 0;
|
||||
// disable note
|
||||
if (device.midiInfo->states_events[midi_index]) {
|
||||
device.midiInfo->states[midi_index] = false;
|
||||
}
|
||||
device.updated = true;
|
||||
} else {
|
||||
// v2 logic
|
||||
// exactly the same as NOTE ON with 0 velocity
|
||||
// velocity is kept; api will ignore it if button is not pressed
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
||||
device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds();
|
||||
device.updated = true;
|
||||
}
|
||||
// for v2_drum, NOTE OFF is ignored
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 0x9: { // NOTE ON
|
||||
|
||||
// param mapping
|
||||
const auto midi_note = midi_byte1 & 127u;
|
||||
|
||||
// per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF.
|
||||
const auto midi_velocity = midi_byte2 & 127u;
|
||||
|
||||
// log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity);
|
||||
|
||||
// get index
|
||||
const auto midi_index = midi_status_channel * 128 + midi_note;
|
||||
if (midi_index < 16 * 128) {
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||
// update velocity
|
||||
device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity;
|
||||
|
||||
if (midi_velocity) {
|
||||
// update events (for legacy logic)
|
||||
// how does this work? see the comment in api.cpp around the check for
|
||||
// get_midi_algorithm() for an explanation
|
||||
|
||||
// so currently it's meant to be turned on
|
||||
device.midiInfo->states[midi_index] = true;
|
||||
|
||||
// if its already on just increase it by one to turn it off
|
||||
if (device.midiInfo->states_events[midi_index] % 2)
|
||||
device.midiInfo->states_events[midi_index]++;
|
||||
else
|
||||
device.midiInfo->states_events[midi_index] += 2;
|
||||
|
||||
} else if (!device.midiInfo->freeze) {
|
||||
// velocity 0 means turn it off
|
||||
device.midiInfo->states[midi_index] = false;
|
||||
}
|
||||
device.updated = true;
|
||||
|
||||
} else {
|
||||
// v2 logic
|
||||
const auto now = get_performance_milliseconds();
|
||||
auto threshold = device.midiInfo->v2_velocity_threshold[midi_index];
|
||||
// when device is frozen (binding is happening) ignore the velocity threshold
|
||||
// this allows users to bind keys even if the midi note is set to high threshold at
|
||||
// rawinput layer, either from a previous binding that was cleared, or existing binding
|
||||
// for another button
|
||||
if (device.midiInfo->freeze) {
|
||||
threshold = 0;
|
||||
}
|
||||
if (threshold < midi_velocity) {
|
||||
device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity;
|
||||
device.midiInfo->v2_last_on_time[midi_index] = now;
|
||||
|
||||
// disable holds and release all notes immediately
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) {
|
||||
device.midiInfo->v2_last_off_time[midi_index] = now;
|
||||
}
|
||||
device.updated = true;
|
||||
} else {
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
||||
// insufficient velocity ON == exactly the same as NOTE OFF
|
||||
device.midiInfo->v2_last_off_time[midi_index] = now;
|
||||
device.updated = true;
|
||||
}
|
||||
// for v2_drum, NOTE ON with insufficient velocity is ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 0xA: // POLYPHONIC PRESSURE
|
||||
break; // skipped above (!)
|
||||
case 0xB: { // CONTROL CHANGE
|
||||
|
||||
// param mapping
|
||||
auto midi_control = midi_byte1 & 127;
|
||||
auto midi_value = midi_byte2 & 127u;
|
||||
|
||||
// get index
|
||||
auto channel_offset = midi_status_channel * 128;
|
||||
auto midi_index = channel_offset + midi_control;
|
||||
if (midi_index < 16 * 128) {
|
||||
|
||||
// continuous controller MSB
|
||||
if (midi_control >= 0x00 && midi_control <= 0x1F) {
|
||||
|
||||
// update index
|
||||
midi_index = midi_status_channel * 32 + midi_control;
|
||||
device.midiInfo->controls_precision_set[midi_index] = true;
|
||||
|
||||
// check if MSB wasn't sent yet
|
||||
if (!device.midiInfo->controls_precision_msb[midi_index]) {
|
||||
device.midiInfo->controls_precision_msb[midi_index] = true;
|
||||
|
||||
// move LSB value to actual position
|
||||
device.midiInfo->controls_precision[midi_index] >>= 7u;
|
||||
}
|
||||
|
||||
// update MSB
|
||||
auto tmp = device.midiInfo->controls_precision[midi_index];
|
||||
tmp = (tmp & 127u) | midi_value << 7u;
|
||||
if (!device.midiInfo->controls_precision_lsb[midi_index])
|
||||
tmp = (tmp & (127u << 7u)) | midi_value;
|
||||
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
||||
device.midiInfo->controls_precision[midi_index] = tmp;
|
||||
device.updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// continuous controller LSB
|
||||
else if (midi_control >= 0x20 && midi_control <= 0x3F) {
|
||||
|
||||
// update index
|
||||
midi_index = midi_status_channel * 32 + midi_control - 0x20;
|
||||
device.midiInfo->controls_precision_set[midi_index] = true;
|
||||
device.midiInfo->controls_precision_lsb[midi_index] = true;
|
||||
|
||||
// check for MSB flag
|
||||
if (device.midiInfo->controls_precision_msb[midi_index]) {
|
||||
|
||||
// update LSB only
|
||||
auto tmp = device.midiInfo->controls_precision[midi_index];
|
||||
tmp &= 127u << 7u;
|
||||
tmp |= midi_value;
|
||||
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
||||
device.midiInfo->controls_precision[midi_index] = tmp;
|
||||
device.updated = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// cast to MSB
|
||||
if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) {
|
||||
device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value;
|
||||
device.updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on/off controls
|
||||
else if (midi_control >= 0x40 && midi_control <= 0x45) {
|
||||
|
||||
// update index
|
||||
midi_index = midi_status_channel * 6 + midi_control - 0x40;
|
||||
device.midiInfo->controls_onoff_set[midi_index] = true;
|
||||
|
||||
// get on/off state
|
||||
const auto onoff_state = midi_value >= 64;
|
||||
|
||||
// update device
|
||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||
if (device.midiInfo->controls_onoff[midi_index] != onoff_state) {
|
||||
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
||||
device.updated = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
// v2 and v2_drum:
|
||||
// unlike notes (drum pads), controls can send continuous ON signal
|
||||
// therefore, check for rising and falling edges
|
||||
const auto now = get_performance_milliseconds();
|
||||
const auto previous_value = device.midiInfo->controls_onoff[midi_index];
|
||||
if (!previous_value && onoff_state) {
|
||||
device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now;
|
||||
device.updated = true;
|
||||
} else if (previous_value && !onoff_state) {
|
||||
device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now;
|
||||
device.updated = true;
|
||||
}
|
||||
|
||||
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
||||
}
|
||||
}
|
||||
|
||||
// single byte controllers
|
||||
else if (midi_control >= 0x46 && midi_control <= 0x5F) {
|
||||
|
||||
// update index
|
||||
midi_index = midi_status_channel * 44 + midi_control - 0x46;
|
||||
device.midiInfo->controls_single_set[midi_index] = true;
|
||||
|
||||
// update device
|
||||
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
||||
device.midiInfo->controls_single[midi_index] = midi_value;
|
||||
device.updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// increment/decrement and parameter numbers
|
||||
else if (midi_control >= 0x60 && midi_control <= 0x65) {
|
||||
// skip
|
||||
}
|
||||
|
||||
// undefined single-byte controllers
|
||||
else if (midi_control >= 0x66 && midi_control <= 0x77) {
|
||||
|
||||
// update index
|
||||
auto sbc_count = 0x5F - 0x46 + 1;
|
||||
midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count;
|
||||
device.midiInfo->controls_single_set[midi_index] = true;
|
||||
|
||||
// update device
|
||||
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
||||
device.midiInfo->controls_single[midi_index] = midi_value;
|
||||
device.updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// channel mode messages
|
||||
else if (midi_control >= 0x78 && midi_control <= 0x7F) {
|
||||
switch (midi_control) {
|
||||
case 0x78: // all sound off
|
||||
break;
|
||||
case 0x79: { // reset all controllers
|
||||
for (int i = 0; i < 32; i++)
|
||||
device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0;
|
||||
for (int i = 0; i < 44; i++)
|
||||
device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
const auto index = midi_status_channel * 6 + i;
|
||||
device.midiInfo->controls_onoff[index] = false;
|
||||
device.midiInfo->v2_controls_onoff_last_on_time[index] = 0;
|
||||
device.midiInfo->v2_controls_onoff_last_off_time[index] = 0;
|
||||
}
|
||||
device.updated = true;
|
||||
break;
|
||||
}
|
||||
case 0x7A: // local control on/off
|
||||
break;
|
||||
case 0x7B: // all notes off
|
||||
case 0x7C: // omni mode off + all notes off
|
||||
case 0x7D: // omni mode on + all notes off
|
||||
case 0x7E: // mono mode on + poly off + all notes off
|
||||
case 0x7F: // poly mode on + mono off + all notes off
|
||||
for (int i = 0; i < 128; i++) {
|
||||
// common
|
||||
device.midiInfo->velocity[channel_offset + i] = 0;
|
||||
device.midiInfo->bind_states[channel_offset + i] = false;
|
||||
|
||||
// legacy
|
||||
device.midiInfo->states[channel_offset + i] = false;
|
||||
device.midiInfo->states_events[channel_offset + i] = 0;
|
||||
|
||||
// v2
|
||||
device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0;
|
||||
device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0;
|
||||
}
|
||||
device.updated = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xC: // PROGRAM CHANGE
|
||||
break; // skipped above (!)
|
||||
case 0xD: // CHANNEL PRESSURE
|
||||
break; // skipped above (!)
|
||||
case 0xE: { // PITCH BENDING
|
||||
|
||||
// raw values range from [0, 0x3FFF] (16383)
|
||||
// build value, centered around zero [-8192, 8191]
|
||||
int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000;
|
||||
|
||||
// update device
|
||||
if (device.midiInfo->pitch_bend[midi_status_channel] != value) {
|
||||
device.midiInfo->pitch_bend[midi_status_channel] = value;
|
||||
device.midiInfo->pitch_bend_set[midi_status_channel] = true;
|
||||
device.updated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xF: // SYSTEM EXCLUSIVE
|
||||
break; // skipped above (!)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// don't iterate through the other devices
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MIM_LONGDATA:
|
||||
case MIM_ERROR:
|
||||
case MIM_LONGERROR:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
+306
-747
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -10,6 +12,7 @@
|
||||
|
||||
#include "device.h"
|
||||
#include "hotplug.h"
|
||||
#include "rawinput_handles.h"
|
||||
#include "rawinput/xinput.h"
|
||||
#include "util/scope_guard.h"
|
||||
|
||||
@@ -63,10 +66,41 @@ namespace rawinput {
|
||||
class RawInputManager {
|
||||
private:
|
||||
|
||||
HotplugManager *hotplug;
|
||||
std::vector<Device> devices;
|
||||
HotplugManager *hotplug = nullptr;
|
||||
|
||||
// std::list (not std::vector) so adding/removing a device never invalidates
|
||||
// pointers to other elements. devices_get() hands out pointers that escape the
|
||||
// lock and are dereferenced later by pollers, so a concurrent scan must not
|
||||
// relocate existing devices
|
||||
std::list<Device> devices;
|
||||
std::recursive_mutex devices_mutex;
|
||||
|
||||
// separate lookup isolated from devices_mutex for the latency-sensitive WM_INPUT path
|
||||
RawInputHandles rawinput_handles;
|
||||
|
||||
WNDCLASSEX input_hwnd_class {};
|
||||
std::thread *input_thread = nullptr;
|
||||
std::thread *midi_thread = nullptr;
|
||||
|
||||
// guards the MIDI scan scheduler flags below. only held around the flag
|
||||
// checks, never across the (slow) enumeration, so it makes "is a scan
|
||||
// running?" and the worker's "exit or rescan?" a single atomic decision -
|
||||
// a request can never slip into the gap between the two
|
||||
std::mutex midi_scan_m;
|
||||
|
||||
// true while a scan worker thread is running. only one runs at a time
|
||||
bool midi_scan_active = false;
|
||||
|
||||
// set when a scan is requested while one is already running, so the worker
|
||||
// rescans once instead of dropping the request (events that arrive during
|
||||
// the slow initial scan would otherwise be lost)
|
||||
bool midi_scan_pending = false;
|
||||
|
||||
// MIDI handles pending close. devices_destruct() runs under devices_mutex but
|
||||
// midiInReset/midiInClose must not (they block on the MIDI callback, which
|
||||
// also takes devices_mutex), so handles are queued here and closed by
|
||||
// midi_close_deferred_flush() once the lock is released
|
||||
std::vector<HMIDIIN> midi_close_deferred;
|
||||
std::thread *flush_thread = nullptr;
|
||||
bool flush_thread_running = false;
|
||||
std::mutex flush_thread_m;
|
||||
@@ -83,6 +117,8 @@ namespace rawinput {
|
||||
void input_hwnd_create();
|
||||
void input_hwnd_destroy();
|
||||
void devices_reload();
|
||||
void midi_scan_join();
|
||||
void midi_close_deferred_flush();
|
||||
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
|
||||
void devices_scan_piuio();
|
||||
void devices_scan_smxstage();
|
||||
@@ -93,6 +129,7 @@ namespace rawinput {
|
||||
void flush_stop();
|
||||
void output_start();
|
||||
void output_stop();
|
||||
void devices_write_output_snapshot(bool only_updated);
|
||||
|
||||
static std::string rawinput_get_device_name(HANDLE hDevice);
|
||||
static std::string rawinput_get_device_description(const DeviceInfo& info, const std::string &device_name);
|
||||
@@ -101,6 +138,10 @@ namespace rawinput {
|
||||
static void CALLBACK input_midi_proc(HMIDIIN, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
|
||||
static DeviceInfo get_device_info(const std::string &device_name);
|
||||
|
||||
// replace an already-destructed slot in place, holding both device locks so
|
||||
// concurrent pollers and the output thread never see a half-copied Device
|
||||
static void replace_device_slot(Device &existing, Device &replacement);
|
||||
|
||||
public:
|
||||
|
||||
HWND input_hwnd = nullptr;
|
||||
@@ -118,6 +159,7 @@ namespace rawinput {
|
||||
|
||||
void devices_scan_rawinput(const std::string &device_name = "");
|
||||
void devices_scan_midi();
|
||||
void midi_scan_start();
|
||||
void devices_scan_xinput();
|
||||
void devices_remove(const std::string &name);
|
||||
|
||||
@@ -130,11 +172,12 @@ namespace rawinput {
|
||||
void __stdcall devices_print();
|
||||
Device *devices_get(const std::string &name, bool updated = false);
|
||||
|
||||
inline std::vector<Device> &devices_get() {
|
||||
inline std::list<Device> &devices_get() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
inline std::vector<Device *> devices_get_updated() {
|
||||
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||
std::vector<Device *> updated;
|
||||
for (auto &device : devices_get()) {
|
||||
device.mutex->lock();
|
||||
@@ -150,8 +193,10 @@ namespace rawinput {
|
||||
}
|
||||
|
||||
inline void devices_midi_freeze(bool freeze) {
|
||||
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||
for (auto &device : devices_get()) {
|
||||
if (device.type == MIDI) {
|
||||
std::lock_guard<std::mutex> device_lock(*device.mutex);
|
||||
device.midiInfo->freeze = freeze;
|
||||
if (!freeze) {
|
||||
for (unsigned short index = 0; index < device.midiInfo->states.size(); index++) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "rawinput_handles.h"
|
||||
|
||||
void rawinput::RawInputHandles::add(Device *device) {
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
this->devices[device->handle] = device;
|
||||
}
|
||||
|
||||
void rawinput::RawInputHandles::remove(Device *device) {
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
// teardown is shared by every device type, and handles can be reused; only erase
|
||||
// an entry that still belongs to this exact RawInput device
|
||||
auto it = this->devices.find(device->handle);
|
||||
if (it != this->devices.end() && it->second == device) {
|
||||
this->devices.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
rawinput::RawInputHandles::AcquiredDevice rawinput::RawInputHandles::acquire(HANDLE handle) {
|
||||
std::lock_guard<std::mutex> index_lock(this->mutex);
|
||||
auto it = this->devices.find(handle);
|
||||
if (it == this->devices.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto *device = it->second;
|
||||
|
||||
// take the device mutex while index_lock is still held so teardown can't free it
|
||||
// between the lookup and the lock
|
||||
return {device, std::unique_lock<std::mutex>(*device->mutex)};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "device.h"
|
||||
|
||||
namespace rawinput {
|
||||
|
||||
class RawInputHandles {
|
||||
public:
|
||||
struct AcquiredDevice {
|
||||
Device *device = nullptr;
|
||||
std::unique_lock<std::mutex> lock;
|
||||
};
|
||||
|
||||
void add(Device *device);
|
||||
void remove(Device *device);
|
||||
AcquiredDevice acquire(HANDLE handle);
|
||||
|
||||
private:
|
||||
// non-owning index; RawInputManager owns the stable device slots
|
||||
std::unordered_map<HANDLE, Device *> devices;
|
||||
std::mutex mutex;
|
||||
};
|
||||
}
|
||||
+172
-22
@@ -24,13 +24,118 @@ namespace rawinput::touch {
|
||||
// settings
|
||||
bool DISABLED = false;
|
||||
bool INVERTED = false;
|
||||
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
|
||||
bool ASPECT_COMPENSATION_GAME = false;
|
||||
|
||||
// state
|
||||
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
|
||||
long DISPLAY_SIZE_X = 1920L;
|
||||
long DISPLAY_SIZE_Y = 1080L;
|
||||
long DISPLAY_NATIVE_X = 0L;
|
||||
long DISPLAY_NATIVE_Y = 0L;
|
||||
bool DISPLAY_INITIALIZED = false;
|
||||
|
||||
static void update_current_display_mode();
|
||||
|
||||
bool aspect_compensation_enabled() {
|
||||
switch (ASPECT_COMPENSATION_MODE) {
|
||||
case AspectMode::On:
|
||||
return true;
|
||||
case AspectMode::Off:
|
||||
return false;
|
||||
default:
|
||||
return ASPECT_COMPENSATION_GAME;
|
||||
}
|
||||
}
|
||||
|
||||
// compute the centered letterbox inset (fraction cropped from each side) that results
|
||||
// from showing the current display mode on a panel with a different native aspect
|
||||
// ratio. only one of inset_x / inset_y is ever non-zero.
|
||||
static void compute_letterbox_inset(double &inset_x, double &inset_y) {
|
||||
inset_x = 0.0;
|
||||
inset_y = 0.0;
|
||||
|
||||
// native resolution in the current display orientation
|
||||
long native_x = DISPLAY_NATIVE_X;
|
||||
long native_y = DISPLAY_NATIVE_Y;
|
||||
if ((DISPLAY_SIZE_Y >= DISPLAY_SIZE_X) != (native_y >= native_x)) {
|
||||
long tmp = native_x;
|
||||
native_x = native_y;
|
||||
native_y = tmp;
|
||||
}
|
||||
|
||||
// compare the displayed aspect ratio against the panel's native one; ignore
|
||||
// tiny mismatches (e.g. 1366x768 vs true 16:9) that would only add a sub-pixel
|
||||
// inset, so near-matched panels stay an exact no-op
|
||||
double current_aspect = (double) DISPLAY_SIZE_X / (double) DISPLAY_SIZE_Y;
|
||||
double native_aspect = (double) native_x / (double) native_y;
|
||||
constexpr double aspect_epsilon = 0.01;
|
||||
if (current_aspect < native_aspect - aspect_epsilon) {
|
||||
|
||||
// image is narrower than the panel -> bars on left/right
|
||||
inset_x = (1.0 - current_aspect / native_aspect) / 2.0;
|
||||
} else if (current_aspect > native_aspect + aspect_epsilon) {
|
||||
|
||||
// image is wider than the panel -> bars on top/bottom
|
||||
inset_y = (1.0 - native_aspect / current_aspect) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// undo letterboxing: remap the normalized (0.0 - 1.0) digitizer coordinates, which span
|
||||
// the whole physical panel, into the displayed image region. returns false when the
|
||||
// touch lands in the black bars (outside the image) and should be ignored.
|
||||
static bool remap_aspect_compensation(double &norm_x, double &norm_y) {
|
||||
if (DISPLAY_NATIVE_X <= 0 || DISPLAY_NATIVE_Y <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
double inset_x, inset_y;
|
||||
compute_letterbox_inset(inset_x, inset_y);
|
||||
|
||||
// log once the first time the compensation actually engages (non-zero inset)
|
||||
static bool logged_active = false;
|
||||
if (!logged_active && (inset_x > 0.0 || inset_y > 0.0)) {
|
||||
logged_active = true;
|
||||
log_info("rawinput", "aspect compensation active: display {}x{}, native {}x{}, inset x={:.4f} y={:.4f}",
|
||||
(int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y,
|
||||
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y,
|
||||
inset_x, inset_y);
|
||||
}
|
||||
|
||||
// ignore touches inside the black bars (outside the displayed image)
|
||||
if ((inset_x > 0.0 && (norm_x < inset_x || norm_x > 1.0 - inset_x)) ||
|
||||
(inset_y > 0.0 && (norm_y < inset_y || norm_y > 1.0 - inset_y))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remap the displayed image region back to the full 0.0 - 1.0 range
|
||||
if (inset_x > 0.0) {
|
||||
norm_x = (norm_x - inset_x) / (1.0 - 2.0 * inset_x);
|
||||
}
|
||||
if (inset_y > 0.0) {
|
||||
norm_y = (norm_y - inset_y) / (1.0 - 2.0 * inset_y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// determine the native (maximum) resolution advertised by the primary display device.
|
||||
// used to detect and undo letterboxing when a non-native display mode is in use.
|
||||
// falls back to the current display size if enumeration yields nothing larger.
|
||||
static void detect_native_resolution() {
|
||||
DISPLAY_NATIVE_X = DISPLAY_SIZE_X;
|
||||
DISPLAY_NATIVE_Y = DISPLAY_SIZE_Y;
|
||||
|
||||
DEVMODE native_mode{};
|
||||
native_mode.dmSize = sizeof(native_mode);
|
||||
for (int i = 0; EnumDisplaySettings(nullptr, i, &native_mode); i++) {
|
||||
if ((long) native_mode.dmPelsWidth * (long) native_mode.dmPelsHeight >
|
||||
DISPLAY_NATIVE_X * DISPLAY_NATIVE_Y) {
|
||||
DISPLAY_NATIVE_X = (long) native_mode.dmPelsWidth;
|
||||
DISPLAY_NATIVE_Y = (long) native_mode.dmPelsHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_touchscreen(Device *device) {
|
||||
|
||||
// check if disabled
|
||||
@@ -176,7 +281,7 @@ namespace rawinput::touch {
|
||||
|
||||
// check if display is initialized
|
||||
if (!DISPLAY_INITIALIZED) {
|
||||
display_update();
|
||||
update_current_display_mode();
|
||||
}
|
||||
|
||||
// update timeouts here as well so events are in the right order
|
||||
@@ -322,6 +427,18 @@ namespace rawinput::touch {
|
||||
std::vector<DWORD> touch_removes;
|
||||
std::vector<TouchPoint> touch_writes;
|
||||
std::vector<DWORD> touch_modifications;
|
||||
|
||||
// drop every touch point with the given id (marking it as released)
|
||||
auto remove_touch_point = [&] (DWORD id) {
|
||||
touch_removes.push_back(id);
|
||||
touch.touch_points.erase(std::remove_if(
|
||||
touch.touch_points.begin(),
|
||||
touch.touch_points.end(),
|
||||
[id] (const HIDTouchPoint &x) {
|
||||
return x.id == id;
|
||||
}), touch.touch_points.end());
|
||||
};
|
||||
|
||||
for (auto &hid_tp : touch_points) {
|
||||
|
||||
// check if existing
|
||||
@@ -353,23 +470,29 @@ namespace rawinput::touch {
|
||||
|
||||
// only remove if it exists
|
||||
if (existing != touch.touch_points.end()) {
|
||||
|
||||
// remove all touch points with this ID
|
||||
touch_removes.push_back(hid_tp.id);
|
||||
touch.touch_points.erase(std::remove_if(
|
||||
touch.touch_points.begin(),
|
||||
touch.touch_points.end(),
|
||||
[hid_tp] (const HIDTouchPoint &x) {
|
||||
return x.id == hid_tp.id;
|
||||
}), touch.touch_points.end());
|
||||
remove_touch_point(hid_tp.id);
|
||||
}
|
||||
} else {
|
||||
|
||||
// write touch point
|
||||
// start from the normalized (0.0 - 1.0) digitizer coordinates
|
||||
double norm_x = hid_tp.x;
|
||||
double norm_y = hid_tp.y;
|
||||
|
||||
// undo letterboxing so raw input touches line up with the displayed
|
||||
// image; touches that land in the black bars are treated as released
|
||||
if (aspect_compensation_enabled() && !remap_aspect_compensation(norm_x, norm_y)) {
|
||||
if (existing != touch.touch_points.end()) {
|
||||
remove_touch_point(hid_tp.id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// scale the normalized coordinates to the monitor to obtain the
|
||||
// physical screen position of the touch
|
||||
TouchPoint tp {
|
||||
.id = hid_tp.id,
|
||||
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
|
||||
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
|
||||
.x = (long) (norm_x * DISPLAY_SIZE_X),
|
||||
.y = (long) (norm_y * DISPLAY_SIZE_Y),
|
||||
.mouse = false,
|
||||
};
|
||||
touch_writes.push_back(tp);
|
||||
@@ -390,13 +513,28 @@ namespace rawinput::touch {
|
||||
}
|
||||
|
||||
// set TTL and last report time
|
||||
if (!touch.elements_x.empty() && !touch_modifications.empty()) {
|
||||
if (!touch.elements_x.empty()) {
|
||||
|
||||
// a HID digitizer reports only a fixed number of contacts per report and
|
||||
// cycles the remaining ones through following reports, so a contact can be
|
||||
// absent for up to this many reports before it comes back around
|
||||
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
|
||||
auto system_time_ms = get_system_milliseconds();
|
||||
|
||||
// extend every tracked contact to the current cycle length without ever
|
||||
// shrinking it; otherwise a rising contact count (e.g. rapid taps) lengthens
|
||||
// the cycle and can starve a stationary held contact into being reaped before
|
||||
// it is reported again. the 50ms last-report timeout remains the hard backstop
|
||||
for (auto &hid_tp : touch.touch_points) {
|
||||
if (hid_tp.ttl < ttl) {
|
||||
hid_tp.ttl = ttl;
|
||||
}
|
||||
}
|
||||
|
||||
// only contacts actually seen this report refresh their last-report time
|
||||
for (auto &hid_tp_id : touch_modifications) {
|
||||
for (auto &hid_tp : touch.touch_points) {
|
||||
if (hid_tp.id == hid_tp_id) {
|
||||
hid_tp.ttl = ttl;
|
||||
hid_tp.last_report = system_time_ms;
|
||||
}
|
||||
}
|
||||
@@ -502,18 +640,13 @@ namespace rawinput::touch {
|
||||
return false;
|
||||
}
|
||||
|
||||
void display_update() {
|
||||
|
||||
// check if disabled
|
||||
if (DISABLED)
|
||||
return;
|
||||
|
||||
static void update_current_display_mode() {
|
||||
// determine monitor size
|
||||
static RECT display_rect;
|
||||
GetWindowRect(GetDesktopWindow(), &display_rect);
|
||||
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
|
||||
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
|
||||
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
|
||||
log_info("rawinput", "primary display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
|
||||
|
||||
// determine monitor orientation
|
||||
DEVMODE display_mode{};
|
||||
@@ -561,4 +694,21 @@ namespace rawinput::touch {
|
||||
// mark as initialized
|
||||
DISPLAY_INITIALIZED = true;
|
||||
}
|
||||
|
||||
void display_update() {
|
||||
|
||||
// check if disabled
|
||||
if (DISABLED)
|
||||
return;
|
||||
|
||||
update_current_display_mode();
|
||||
|
||||
if (aspect_compensation_enabled()) {
|
||||
// determine the native (maximum) resolution of the primary display; used to
|
||||
// detect and undo letterboxing when a non-native display mode is in use
|
||||
detect_native_resolution();
|
||||
log_info("rawinput", "primary display native size: {}x{}",
|
||||
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,25 @@ namespace rawinput::touch {
|
||||
extern bool DISABLED;
|
||||
extern bool INVERTED;
|
||||
|
||||
// aspect-ratio compensation: when the current display mode has a different aspect
|
||||
// ratio than the panel's native resolution, the image is letterboxed/pillarboxed
|
||||
// on the physical panel while the digitizer still spans the whole panel. this
|
||||
// remaps the touch into the displayed image region.
|
||||
enum class AspectMode {
|
||||
Auto, // decided per game via ASPECT_COMPENSATION_GAME
|
||||
On, // always compensate
|
||||
Off, // never compensate
|
||||
};
|
||||
|
||||
// global override, driven by the -rawtouchaspect launcher option
|
||||
extern AspectMode ASPECT_COMPENSATION_MODE;
|
||||
|
||||
// per-game request, honored only when ASPECT_COMPENSATION_MODE is Auto
|
||||
extern bool ASPECT_COMPENSATION_GAME;
|
||||
|
||||
// effective decision combining the global mode and the per-game request
|
||||
bool aspect_compensation_enabled();
|
||||
|
||||
// global state
|
||||
extern DWORD DISPLAY_ORIENTATION;
|
||||
extern long DISPLAY_SIZE_X;
|
||||
|
||||
@@ -179,11 +179,27 @@ XInputSetState(
|
||||
|
||||
XInputManager::XInputManager() {
|
||||
log_info("xinput", "initialize...");
|
||||
this->xinput_lib = LoadLibraryA("xinput1_3.dll");
|
||||
|
||||
// many systems ship only a newer XInput runtime, so try the known
|
||||
// DLL names in order of preference until one loads
|
||||
static const char *xinput_dll_names[] = {
|
||||
"xinput1_4.dll",
|
||||
"xinput1_3.dll",
|
||||
"xinput9_1_0.dll",
|
||||
};
|
||||
const char *loaded_name = nullptr;
|
||||
for (const char *name : xinput_dll_names) {
|
||||
this->xinput_lib = LoadLibraryA(name);
|
||||
if (this->xinput_lib) {
|
||||
loaded_name = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this->xinput_lib) {
|
||||
log_warning("xinput", "failed to load xinput1_3.dll");
|
||||
log_warning("xinput", "failed to load any XInput DLL");
|
||||
return;
|
||||
}
|
||||
log_info("xinput", "loaded {}", loaded_name);
|
||||
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
|
||||
GetProcAddress(this->xinput_lib, "XInputGetState"));
|
||||
if (!XInputGetState_addr) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user