mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 14:50:41 -07:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,6 +412,7 @@ 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
|
||||
@@ -431,6 +432,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
|
||||
@@ -618,6 +620,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
rawinput/touch.cpp
|
||||
@@ -644,8 +647,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
stubs/stubs.cpp
|
||||
|
||||
# touch
|
||||
touch/gdi_overlay.cpp
|
||||
touch/touch.cpp
|
||||
touch/touch_indicators.cpp
|
||||
touch/touch_gestures.cpp
|
||||
touch/win7.cpp
|
||||
touch/win8.cpp
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -411,6 +411,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;
|
||||
}
|
||||
|
||||
+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,470 @@
|
||||
#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));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// let the rawinput stack correct the aspect ratio
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = 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);
|
||||
}
|
||||
@@ -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);
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
} else {
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} 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;
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -68,6 +68,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)) {
|
||||
@@ -130,5 +139,9 @@ namespace hooks::audio {
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#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"
|
||||
@@ -627,7 +627,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 +722,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -1064,6 +1065,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 +1115,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) {
|
||||
@@ -1305,21 +1318,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();
|
||||
}
|
||||
|
||||
@@ -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 %",
|
||||
@@ -1031,16 +1043,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",
|
||||
},
|
||||
@@ -1772,11 +1784,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 +1808,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 +1837,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 +1916,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. "
|
||||
@@ -2668,6 +2700,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 +2708,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 +2774,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 +2790,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",
|
||||
|
||||
@@ -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,
|
||||
@@ -262,6 +264,9 @@ namespace launcher {
|
||||
IIDXWindowedSubscreenAlwaysOnTop,
|
||||
spice2x_JubeatLegacyTouch,
|
||||
JubeatTouchAlgo,
|
||||
JubeatTouchDebug,
|
||||
JubeatTouchDebounce,
|
||||
RBTouchDebug,
|
||||
spice2x_RBTouchScale,
|
||||
RBTouchSize,
|
||||
RBTouchPollRate,
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.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"
|
||||
@@ -66,6 +67,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 +313,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;
|
||||
}
|
||||
@@ -551,9 +556,13 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_fps->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_rb_touch_debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,6 +587,10 @@ 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) {
|
||||
@@ -595,6 +608,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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace rawinput {
|
||||
switch (hdr->dbch_devicetype) {
|
||||
case DBT_DEVTYP_DEVICEINTERFACE: {
|
||||
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
|
||||
this->ri_mgr->devices_scan_midi();
|
||||
this->ri_mgr->midi_scan_start();
|
||||
|
||||
// check if class is not HID
|
||||
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
|
||||
@@ -112,6 +112,7 @@ namespace rawinput {
|
||||
std::string name(dev->dbcc_name);
|
||||
|
||||
// destruct device
|
||||
log_misc("hotplug", "device interface removal: {}", name);
|
||||
this->ri_mgr->devices_remove(name);
|
||||
}
|
||||
}
|
||||
@@ -120,8 +121,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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
#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.mutex = new std::mutex();
|
||||
midi_device.mutex_out = new std::mutex();
|
||||
midi_device.midiInfo = midi_device_midi_info;
|
||||
|
||||
// mutate the shared device list under lock (the slow WinMM calls above
|
||||
// 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, reusing the slot's existing mutexes
|
||||
this->devices_destruct(&device);
|
||||
reuse_device_mutexes(midi_device, device);
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
+261
-712
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>
|
||||
@@ -63,10 +65,38 @@ 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;
|
||||
|
||||
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 +113,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 +125,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 +134,11 @@ namespace rawinput {
|
||||
static void CALLBACK input_midi_proc(HMIDIIN, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
|
||||
static DeviceInfo get_device_info(const std::string &device_name);
|
||||
|
||||
// shared by the rawinput / xinput / midi device scans when replacing a slot
|
||||
// in place; keeps the existing slot's mutex pair so snapshot pointers held by
|
||||
// other threads stay valid (see the definition in rawinput.cpp)
|
||||
static void reuse_device_mutexes(Device &replacement, const Device &existing);
|
||||
|
||||
public:
|
||||
|
||||
HWND input_hwnd = nullptr;
|
||||
@@ -118,6 +156,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 +169,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,6 +190,7 @@ 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) {
|
||||
device.midiInfo->freeze = freeze;
|
||||
|
||||
+157
-15
@@ -24,13 +24,116 @@ 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;
|
||||
|
||||
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
|
||||
@@ -322,6 +425,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 +468,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 +511,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;
|
||||
}
|
||||
}
|
||||
@@ -513,7 +649,13 @@ namespace rawinput::touch {
|
||||
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 the native (maximum) resolution of the primary display; used to
|
||||
// detect and undo letterboxing when a non-native display mode is in use
|
||||
detect_native_resolution();
|
||||
log_info("rawinput", "primary display native size: {}x{}",
|
||||
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
|
||||
|
||||
// determine monitor orientation
|
||||
DEVMODE display_mode{};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#include "gdi_overlay.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
||||
enum class BufferType {
|
||||
TargetCompatible,
|
||||
Bgra32,
|
||||
};
|
||||
|
||||
struct GdiBuffer {
|
||||
HDC dc = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
// bitmap originally selected into the memory DC, restored before cleanup
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
||||
GdiBuffer BACK_BUFFER;
|
||||
GdiBuffer OVERLAY_BUFFER;
|
||||
|
||||
void release_buffer(GdiBuffer &buffer) {
|
||||
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
||||
if (buffer.dc != nullptr) {
|
||||
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
||||
SelectObject(buffer.dc, buffer.old_bitmap);
|
||||
}
|
||||
DeleteDC(buffer.dc);
|
||||
}
|
||||
if (buffer.bitmap != nullptr) {
|
||||
DeleteObject(buffer.bitmap);
|
||||
}
|
||||
|
||||
buffer = {};
|
||||
}
|
||||
|
||||
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
||||
// selected into it. a matching allocation is reused; otherwise the old resources are
|
||||
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
||||
bool ensure_buffer(
|
||||
GdiBuffer &buffer,
|
||||
HDC target_dc,
|
||||
int width,
|
||||
int height,
|
||||
BufferType type,
|
||||
const char *name) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
||||
buffer.width == width && buffer.height == height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// keep allocations across frames and recreate only after a size change
|
||||
release_buffer(buffer);
|
||||
buffer.dc = CreateCompatibleDC(target_dc);
|
||||
if (buffer.dc == nullptr) {
|
||||
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
||||
if (type == BufferType::TargetCompatible) {
|
||||
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
||||
} else {
|
||||
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
||||
}
|
||||
if (buffer.bitmap == nullptr) {
|
||||
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
||||
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
||||
release_buffer(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer.width = width;
|
||||
buffer.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update_overlay_buffer(
|
||||
HDC target_dc,
|
||||
const uint32_t *pixels,
|
||||
bool pixels_dirty,
|
||||
int width,
|
||||
int height) {
|
||||
if (pixels == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
||||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
||||
if (!ensure_buffer(
|
||||
OVERLAY_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::Bgra32,
|
||||
"software overlay")) {
|
||||
return false;
|
||||
}
|
||||
if (!needs_update) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// SetDIBits requires the destination bitmap not to be selected into a DC
|
||||
HGDIOBJ overlay_bitmap =
|
||||
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
||||
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
|
||||
BITMAPINFO bitmap_info {};
|
||||
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bitmap_info.bmiHeader.biWidth = width;
|
||||
bitmap_info.bmiHeader.biHeight = -height;
|
||||
bitmap_info.bmiHeader.biPlanes = 1;
|
||||
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
int copied_lines = SetDIBits(
|
||||
target_dc,
|
||||
OVERLAY_BUFFER.bitmap,
|
||||
0,
|
||||
height,
|
||||
pixels,
|
||||
&bitmap_info,
|
||||
DIB_RGB_COLORS);
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
||||
|
||||
if (copied_lines != height) {
|
||||
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
||||
copied_lines, height);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height) {
|
||||
if (!ensure_buffer(
|
||||
BACK_BUFFER,
|
||||
target_dc,
|
||||
width,
|
||||
height,
|
||||
BufferType::TargetCompatible,
|
||||
"overlay back buffer")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC draw_dc = BACK_BUFFER.dc;
|
||||
SetBkMode(draw_dc, TRANSPARENT);
|
||||
|
||||
// start each frame from the transparent color-key background
|
||||
RECT buffer_rect {0, 0, width, height};
|
||||
FillRect(draw_dc, &buffer_rect, background_brush);
|
||||
|
||||
if (update_overlay_buffer(
|
||||
target_dc,
|
||||
overlay_pixels,
|
||||
overlay_pixels_dirty,
|
||||
overlay_width,
|
||||
overlay_height) &&
|
||||
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
||||
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
||||
}
|
||||
|
||||
return draw_dc;
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_present(HDC target_dc) {
|
||||
// one full-window blit exposes the completed frame without an intermediate erase
|
||||
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
||||
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
void touch_gdi_overlay_release() {
|
||||
release_buffer(BACK_BUFFER);
|
||||
release_buffer(OVERLAY_BUFFER);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
||||
HDC touch_gdi_overlay_begin_frame(
|
||||
HDC target_dc,
|
||||
HBRUSH background_brush,
|
||||
int width,
|
||||
int height,
|
||||
const uint32_t *overlay_pixels,
|
||||
bool overlay_pixels_dirty,
|
||||
int overlay_width,
|
||||
int overlay_height);
|
||||
|
||||
// presents the frame prepared by the most recent successful begin call
|
||||
void touch_gdi_overlay_present(HDC target_dc);
|
||||
|
||||
// releases all cached GDI resources
|
||||
void touch_gdi_overlay_release();
|
||||
+91
-57
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "games/jb/jb.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
@@ -19,9 +20,12 @@
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "gdi_overlay.h"
|
||||
#include "handler.h"
|
||||
#include "touch_gestures.h"
|
||||
#include "win7.h"
|
||||
#include "win8.h"
|
||||
|
||||
@@ -35,6 +39,16 @@ static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
|
||||
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
|
||||
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
|
||||
|
||||
// timer id for the overlay repaint tick
|
||||
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;
|
||||
|
||||
// overlay repaint interval; the WinXP-compat build stays at 30 FPS
|
||||
#if !SPICE_XP
|
||||
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
|
||||
#else
|
||||
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
|
||||
#endif // !SPICE_XP
|
||||
|
||||
// in mainline spicetools, this was false (show by default)
|
||||
// in spice2x, this is true (hide by default)
|
||||
bool SPICETOUCH_CARD_DISABLE = true;
|
||||
@@ -247,16 +261,15 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
if (!SPICETOUCH_REGISTERED_TOUCH) {
|
||||
SPICETOUCH_REGISTERED_TOUCH = true;
|
||||
|
||||
// check if touch is available
|
||||
// register the handler when a touch screen is present
|
||||
if (is_touch_available("SpiceTouchWndProc")) {
|
||||
|
||||
// notify the handler of our window
|
||||
TOUCH_HANDLER->window_register(hWnd);
|
||||
}
|
||||
|
||||
// enable card unless the feature is disabled
|
||||
if (!SPICETOUCH_CARD_DISABLE) {
|
||||
SPICETOUCH_CARD_ENABLED = true;
|
||||
}
|
||||
// enable the card button whenever the option is set, even without a touch
|
||||
// screen (mouse clicks are handled as touch input and can trigger it)
|
||||
if (!SPICETOUCH_CARD_DISABLE) {
|
||||
SPICETOUCH_CARD_ENABLED = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +320,10 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
break;
|
||||
}
|
||||
case WM_TIMER: {
|
||||
InvalidateRect(hWnd, NULL, TRUE);
|
||||
|
||||
// request a repaint; the frame is composed into an offscreen buffer, so no
|
||||
// background erase is needed (bErase = FALSE avoids a transparent flash)
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
break;
|
||||
}
|
||||
case WM_PAINT: {
|
||||
@@ -339,52 +355,52 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
// draw overlay
|
||||
// render the software overlay before BeginPaint so only GDI composition
|
||||
// and the final blit happen while the window is being painted
|
||||
int overlay_width = 0, overlay_height = 0;
|
||||
uint32_t *overlay_pixels = nullptr;
|
||||
bool overlay_pixels_dirty = false;
|
||||
if (overlay_enabled) {
|
||||
|
||||
// update and render
|
||||
overlay::OVERLAY->update();
|
||||
overlay::OVERLAY->new_frame();
|
||||
overlay::OVERLAY->render();
|
||||
|
||||
// get pixel data
|
||||
int width, height;
|
||||
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height);
|
||||
if (width > 0 && height > 0) {
|
||||
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
|
||||
&overlay_width, &overlay_height);
|
||||
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
|
||||
}
|
||||
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
|
||||
|
||||
// create bitmap
|
||||
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
|
||||
// compose the whole frame into an offscreen back buffer and present it with a
|
||||
// single blit; the window never shows a half-erased (transparent) surface
|
||||
// mid-paint, which is what caused the occasional flicker at higher frame rates
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
|
||||
// prepare paint
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
RECT bufferRect {};
|
||||
GetClientRect(hWnd, &bufferRect);
|
||||
int buffer_width = bufferRect.right - bufferRect.left;
|
||||
int buffer_height = bufferRect.bottom - bufferRect.top;
|
||||
|
||||
/*
|
||||
* draw bitmap
|
||||
* - this currently sets the background to black because of SRCCOPY
|
||||
* - SRCPAINT will blend but colors are wrong
|
||||
* - once this is figured out we could also try hooking WM_PAINT and
|
||||
* draw directly to the game window
|
||||
*/
|
||||
SelectObject(hdcMem, bitmap);
|
||||
BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
|
||||
|
||||
// clean up
|
||||
DeleteObject(bitmap);
|
||||
DeleteDC(hdcMem);
|
||||
EndPaint(hWnd, &paint);
|
||||
}
|
||||
HBRUSH color_key_brush =
|
||||
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
|
||||
HDC draw_dc = touch_gdi_overlay_begin_frame(
|
||||
hdc,
|
||||
color_key_brush,
|
||||
buffer_width,
|
||||
buffer_height,
|
||||
overlay_pixels,
|
||||
overlay_pixels_dirty,
|
||||
overlay_width,
|
||||
overlay_height);
|
||||
if (draw_dc == nullptr) {
|
||||
EndPaint(hWnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// draw card input
|
||||
if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) {
|
||||
|
||||
// prepare paint
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
// draw the insert-card button below the jubeat debug overlay; it is
|
||||
// hidden while the overlay is active
|
||||
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
|
||||
|
||||
// create brushes
|
||||
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
|
||||
@@ -414,7 +430,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
SPICETOUCH_CARD_RECT = boxRect;
|
||||
|
||||
// draw borders
|
||||
FillRect(hdc, &boxRect, brushBorder);
|
||||
FillRect(draw_dc, &boxRect, brushBorder);
|
||||
|
||||
// modify box rect
|
||||
boxRect.left += 1;
|
||||
@@ -423,7 +439,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
boxRect.bottom -= 1;
|
||||
|
||||
// fill box
|
||||
FillRect(hdc, &boxRect, brushFill);
|
||||
FillRect(draw_dc, &boxRect, brushFill);
|
||||
|
||||
// modify box rect
|
||||
if (should_rotate) {
|
||||
@@ -435,21 +451,27 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
}
|
||||
|
||||
// draw text
|
||||
SelectObject(hdc, SPICETOUCH_FONT);
|
||||
SetTextColor(hdc, RGB(0, 196, 0));
|
||||
DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
|
||||
SelectObject(draw_dc, SPICETOUCH_FONT);
|
||||
SetTextColor(draw_dc, RGB(0, 196, 0));
|
||||
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
|
||||
|
||||
// delete objects
|
||||
DeleteObject(brushFill);
|
||||
DeleteObject(brushBorder);
|
||||
|
||||
// end paint
|
||||
EndPaint(hWnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call default window procedure
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
#if !SPICE_XP
|
||||
// draw the jubeat debug overlay on top (hidden while the overlay is active)
|
||||
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
|
||||
games::jb::touch_draw_debug_overlay(draw_dc);
|
||||
}
|
||||
#endif // !SPICE_XP
|
||||
|
||||
// present the composed frame in a single blit
|
||||
touch_gdi_overlay_present(hdc);
|
||||
|
||||
EndPaint(hWnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
case WM_CREATE: {
|
||||
|
||||
@@ -479,6 +501,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY: {
|
||||
touch_gdi_overlay_release();
|
||||
if (SPICETOUCH_FONT != nullptr) {
|
||||
DeleteObject(SPICETOUCH_FONT);
|
||||
SPICETOUCH_FONT = nullptr;
|
||||
}
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
@@ -538,6 +565,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
.x = GET_X_LPARAM(lParam),
|
||||
.y = GET_Y_LPARAM(lParam),
|
||||
.mouse = true,
|
||||
.down_ms = get_performance_milliseconds(),
|
||||
};
|
||||
TOUCH_POINTS.push_back(tp);
|
||||
|
||||
@@ -751,6 +779,9 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
|
||||
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
|
||||
UpdateWindow(touch_window);
|
||||
|
||||
// disable the OS touch contact visualization for our own touch window
|
||||
disable_touch_gestures(touch_window);
|
||||
|
||||
// register
|
||||
touch_register_window(touch_window);
|
||||
|
||||
@@ -763,8 +794,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
|
||||
// create instance
|
||||
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
|
||||
|
||||
// draw overlay in 30 FPS
|
||||
SetTimer(touch_window, 1, 1000 / 30, NULL);
|
||||
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
|
||||
SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +879,9 @@ void touch_write_points(std::vector<TouchPoint> *touch_points) {
|
||||
// create new touch point when not found
|
||||
if (!found) {
|
||||
|
||||
// stamp the landing time so debounce can measure the contact's age
|
||||
tp.down_ms = get_performance_milliseconds();
|
||||
|
||||
// add touch point
|
||||
TOUCH_POINTS.push_back(tp);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ struct TouchPoint {
|
||||
DWORD id;
|
||||
LONG x, y;
|
||||
bool mouse;
|
||||
double down_ms = 0.0; // performance-counter milliseconds when the contact first landed
|
||||
};
|
||||
enum TouchEventType {
|
||||
TOUCH_DOWN,
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
|
||||
// set version to Windows 8 to enable Windows 8 touch functions
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include <propsys.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "touch_gestures.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
// tablet/pen service flags (MicrosoftTabletPenServiceProperty atom)
|
||||
// these are not present in the mingw headers
|
||||
#ifndef TABLET_DISABLE_PRESSANDHOLD
|
||||
#define TABLET_DISABLE_PRESSANDHOLD 0x00000001
|
||||
#define TABLET_DISABLE_PENTAPFEEDBACK 0x00000008
|
||||
#define TABLET_DISABLE_PENBARRELFEEDBACK 0x00000010
|
||||
#define TABLET_DISABLE_TOUCHUIFORCEON 0x00000100
|
||||
#define TABLET_DISABLE_TOUCHUIFORCEOFF 0x00000200
|
||||
#define TABLET_DISABLE_TOUCHSWITCH 0x00008000
|
||||
#define TABLET_DISABLE_FLICKS 0x00010000
|
||||
#define TABLET_DISABLE_SMOOTHSCROLLING 0x00080000
|
||||
#define TABLET_DISABLE_FLICKFALLBACKKEYS 0x00100000
|
||||
#endif
|
||||
|
||||
static const char TABLET_ATOM_NAME[] = "MicrosoftTabletPenServiceProperty";
|
||||
|
||||
// PKEY_EdgeGesture_DisableTouchWhenFullscreen format GUID
|
||||
// (not defined in the mingw headers); defined inline to avoid an initguid.h
|
||||
// symbol clash with touch/win8.cpp which declares the same name
|
||||
// {32CE38B2-2C9A-41B1-9BC5-B3784394AA44}
|
||||
static const GUID EDGEGESTURE_DISABLE_FMT =
|
||||
{ 0x32CE38B2, 0x2C9A, 0x41B1, { 0x9B, 0xC5, 0xB3, 0x78, 0x43, 0x94, 0xAA, 0x44 } };
|
||||
|
||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||
typedef BOOL (WINAPI *SetWindowFeedbackSetting_t)(HWND, FEEDBACK_TYPE, DWORD, UINT32, const VOID *);
|
||||
static SetWindowFeedbackSetting_t pSetWindowFeedbackSetting = nullptr;
|
||||
|
||||
static HINSTANCE SHELL32_INSTANCE = nullptr;
|
||||
typedef HRESULT (WINAPI *SHGetPropertyStoreForWindow_t)(HWND, REFIID, void **);
|
||||
static SHGetPropertyStoreForWindow_t pSHGetPropertyStoreForWindow = nullptr;
|
||||
|
||||
static std::once_flag INIT_FLAG;
|
||||
|
||||
// resolve the libraries and entry points once; disable_touch_gestures may be
|
||||
// called concurrently from the CreateWindowEx hooks and the touch thread
|
||||
static void init_procs() {
|
||||
std::call_once(INIT_FLAG, []() {
|
||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||
if (USER32_INSTANCE != nullptr) {
|
||||
pSetWindowFeedbackSetting = libutils::try_proc<SetWindowFeedbackSetting_t>(
|
||||
USER32_INSTANCE, "SetWindowFeedbackSetting");
|
||||
}
|
||||
|
||||
SHELL32_INSTANCE = libutils::try_library("shell32.dll");
|
||||
if (SHELL32_INSTANCE != nullptr) {
|
||||
pSHGetPropertyStoreForWindow = libutils::try_proc<SHGetPropertyStoreForWindow_t>(
|
||||
SHELL32_INSTANCE, "SHGetPropertyStoreForWindow");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void disable_feedback_visuals(HWND hwnd) {
|
||||
|
||||
if (pSetWindowFeedbackSetting == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL enabled = FALSE;
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_TAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_DOUBLETAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_PRESSANDHOLD,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_RIGHTTAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_GESTURE_PRESSANDTAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
}
|
||||
|
||||
static void disable_gesture_behaviors(HWND hwnd) {
|
||||
|
||||
// the tablet/pen service reads this window property to decide which
|
||||
// touch/pen gestures to suppress for the window. this covers:
|
||||
// - press-and-hold (touch right-click / long-press ring)
|
||||
// - pen tap/barrel feedback
|
||||
// - flicks (edge/directional flick navigation gestures)
|
||||
// - the touch keyboard invocation UI
|
||||
// - smooth scrolling / flick fallback keys
|
||||
DWORD tablet_flags = TABLET_DISABLE_PRESSANDHOLD |
|
||||
TABLET_DISABLE_PENTAPFEEDBACK |
|
||||
TABLET_DISABLE_PENBARRELFEEDBACK |
|
||||
TABLET_DISABLE_FLICKS |
|
||||
TABLET_DISABLE_TOUCHUIFORCEOFF |
|
||||
TABLET_DISABLE_TOUCHSWITCH |
|
||||
TABLET_DISABLE_SMOOTHSCROLLING |
|
||||
TABLET_DISABLE_FLICKFALLBACKKEYS;
|
||||
|
||||
ATOM atom_id = GlobalAddAtomA(TABLET_ATOM_NAME);
|
||||
if (atom_id > 0) {
|
||||
SetPropA(hwnd, TABLET_ATOM_NAME, (HANDLE) ((ULONG_PTR) tablet_flags));
|
||||
}
|
||||
}
|
||||
|
||||
static void disable_edge_gestures(HWND hwnd) {
|
||||
|
||||
// suppress the touch edge swipes (charms/back/app bar) for the window while
|
||||
// it is fullscreen. this is normally done by the touch handler's
|
||||
// window_register(), but that is not called for every handler (e.g. the
|
||||
// rawinput handler is a no-op), so apply it here as well.
|
||||
if (pSHGetPropertyStoreForWindow == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
IPropertyStore *ps = nullptr;
|
||||
HRESULT hr = pSHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void **) &ps);
|
||||
if (SUCCEEDED(hr) && ps != nullptr) {
|
||||
PROPERTYKEY key = { EDGEGESTURE_DISABLE_FMT, 2 };
|
||||
|
||||
PROPVARIANT var {};
|
||||
var.vt = VT_BOOL;
|
||||
var.boolVal = VARIANT_TRUE;
|
||||
|
||||
hr = ps->SetValue(key, var);
|
||||
ps->Release();
|
||||
|
||||
if (FAILED(hr)) {
|
||||
log_warning("touch_gestures",
|
||||
"failed to disable edge gestures on HWND={}", fmt::ptr(hwnd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void disable_touch_gestures(HWND hwnd) {
|
||||
|
||||
init_procs();
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_misc("touch_gestures",
|
||||
"disable visual feedback and gestures for touch events for HWND={}", fmt::ptr(hwnd));
|
||||
|
||||
// disable the visual feedback (contact circles, tap/double-tap stars, etc.)
|
||||
disable_feedback_visuals(hwnd);
|
||||
|
||||
// disable the actual gesture behaviors (press-and-hold, flicks, etc.)
|
||||
disable_gesture_behaviors(hwnd);
|
||||
|
||||
// disable the fullscreen touch edge swipes
|
||||
disable_edge_gestures(hwnd);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// disable the OS touch UX for a window: visual feedback (contact circles,
|
||||
// tap/press-and-hold indicators) and gesture behaviors (press-and-hold
|
||||
// right-click, flicks, etc.)
|
||||
void disable_touch_gestures(HWND hwnd);
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
// set version to Windows 8 to enable Windows 8 touch functions
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include "touch_indicators.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||
typedef BOOL (WINAPI *SetWindowFeedbackSetting_t)(HWND, FEEDBACK_TYPE, DWORD, UINT32, const VOID *);
|
||||
static SetWindowFeedbackSetting_t pSetWindowFeedbackSetting = nullptr;
|
||||
|
||||
void disable_touch_indicators(HWND hwnd) {
|
||||
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||
}
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pSetWindowFeedbackSetting == nullptr) {
|
||||
pSetWindowFeedbackSetting = libutils::try_proc<SetWindowFeedbackSetting_t>(
|
||||
USER32_INSTANCE, "SetWindowFeedbackSetting");
|
||||
}
|
||||
if (pSetWindowFeedbackSetting == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("touch_indicators", "disable visual feedback for touch events for HWND={}", fmt::ptr(hwnd));
|
||||
|
||||
BOOL enabled = FALSE;
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_TAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_DOUBLETAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_PRESSANDHOLD,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_TOUCH_RIGHTTAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
pSetWindowFeedbackSetting(
|
||||
hwnd,
|
||||
FEEDBACK_GESTURE_PRESSANDTAP,
|
||||
0, sizeof(enabled), &enabled);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
void disable_touch_indicators(HWND hwnd);
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "rawinput/touch.h"
|
||||
|
||||
// mingw issue #2205 workaround
|
||||
@@ -177,6 +178,7 @@ void Win7Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
|
||||
.x = point.x,
|
||||
.y = point.y,
|
||||
.mouse = false,
|
||||
.down_ms = get_performance_milliseconds(),
|
||||
};
|
||||
TOUCH_POINTS.push_back(tp);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "rawinput/touch.h"
|
||||
|
||||
// mingw does not seem to have this either
|
||||
@@ -114,10 +115,16 @@ bool Win8Handler::is_available() {
|
||||
bool Win8Handler::window_register(HWND hWnd) {
|
||||
|
||||
// atom settings
|
||||
// keep this in sync with touch/touch_gestures.cpp so registering a touch
|
||||
// window does not downgrade the flag set applied there
|
||||
DWORD dwHwndTabletProperty = TABLET_DISABLE_PRESSANDHOLD |
|
||||
TABLET_DISABLE_PENTAPFEEDBACK |
|
||||
TABLET_DISABLE_PENBARRELFEEDBACK |
|
||||
TABLET_DISABLE_FLICKS;
|
||||
TABLET_DISABLE_FLICKS |
|
||||
TABLET_DISABLE_TOUCHUIFORCEOFF |
|
||||
TABLET_DISABLE_TOUCHSWITCH |
|
||||
TABLET_DISABLE_SMOOTHSCROLLING |
|
||||
TABLET_DISABLE_FLICKFALLBACKKEYS;
|
||||
|
||||
// get atom ID
|
||||
ATOM atomID = GlobalAddAtom(ATOM_NAME);
|
||||
@@ -214,6 +221,7 @@ void Win8Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
|
||||
.x = point.x,
|
||||
.y = point.y,
|
||||
.mouse = false,
|
||||
.down_ms = get_performance_milliseconds(),
|
||||
};
|
||||
TOUCH_POINTS.push_back(tp);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# --- configuration ----------------------------------------------------------
|
||||
$scriptDir = if ($env:SPICE_DIR) { $env:SPICE_DIR } elseif ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
||||
$repo = 'spice2x/spice2x.github.io'
|
||||
$targets = @('spice.exe', 'spice64.exe', 'spicecfg.exe')
|
||||
$beta = ($env:SPICE_CHANNEL -match 'beta')
|
||||
$rc = 0
|
||||
|
||||
$title = if ($beta) { '=== spice2x updater (beta channel) ===' } else { '=== spice2x updater ===' }
|
||||
Write-Host "`n$title"
|
||||
Write-Host "Target folder: $scriptDir`n"
|
||||
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$headers = @{ 'User-Agent' = 'spice2x-updater'; 'Accept' = 'application/vnd.github+json' }
|
||||
|
||||
# --- find the newest release (beta = include pre-releases) --------------
|
||||
Write-Host 'Querying latest release...'
|
||||
$url = if ($beta) { "https://api.github.com/repos/$repo/releases?per_page=1" } else { "https://api.github.com/repos/$repo/releases/latest" }
|
||||
$rel = Invoke-RestMethod -Headers $headers -Uri $url | Select-Object -First 1
|
||||
if (-not $rel) { throw 'No releases found.' }
|
||||
|
||||
# --- pick the distribution zip (spice2x-<date>.zip, not the -full one) --
|
||||
$asset = $rel.assets | Where-Object { $_.name -like 'spice2x-*.zip' -and $_.name -notlike '*-full.zip' } | Select-Object -First 1
|
||||
if (-not $asset) { throw 'No .zip asset found in the release.' }
|
||||
Write-Host "Latest release: $($rel.tag_name)$(if ($rel.prerelease) { ' [pre-release]' }) (asset: $($asset.name))"
|
||||
|
||||
# --- download the zip into memory ---------------------------------------
|
||||
# note: on Windows PowerShell 5.1 .Content is a String (empty for binary
|
||||
# responses), so read the raw byte stream instead
|
||||
Write-Host 'Downloading...'
|
||||
$bytes = (Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -UseBasicParsing).RawContentStream.ToArray()
|
||||
|
||||
# --- extract just the three executables straight into this folder -------
|
||||
# PS 5.1 needs these assemblies loaded; PS 7 already has the types (and the
|
||||
# FileSystem assembly name no longer resolves there), so only load if missing
|
||||
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
|
||||
Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
|
||||
}
|
||||
$zip = [IO.Compression.ZipArchive]::new([IO.MemoryStream]::new([byte[]]$bytes))
|
||||
try {
|
||||
$updated = 0
|
||||
foreach ($name in $targets) {
|
||||
$entry = $zip.Entries | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
||||
if (-not $entry) { Write-Warning " $name not found in the archive"; continue }
|
||||
try {
|
||||
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $scriptDir $name), $true)
|
||||
Write-Host " updated $name"; $updated++
|
||||
} catch {
|
||||
Write-Warning " FAILED to write $name (running / read-only?): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
} finally { $zip.Dispose() }
|
||||
|
||||
Write-Host "`nDone. $updated of $($targets.Count) executables updated to $($rel.tag_name)."
|
||||
Write-Host "Only the .exe files are updated; if you copied any DLL stubs, they were not changed."
|
||||
if ($updated -ne $targets.Count) { $rc = 1 }
|
||||
} catch {
|
||||
Write-Host ''; Write-Error $_.Exception.Message; $rc = 1
|
||||
}
|
||||
|
||||
# --- result ------------------------------------------------------------------
|
||||
Write-Host $(if ($rc) { "`nUpdate FAILED. See the error above." } else { "`nUpdate finished successfully." })
|
||||
Start-Sleep -Seconds 5
|
||||
exit $rc
|
||||
Reference in New Issue
Block a user