mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
Compare commits
42 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 | |||
| cf86fbd238 | |||
| 3fdb369128 | |||
| db7defff5a | |||
| 451cbec0b9 | |||
| 5c69e295ab | |||
| 5065a92d55 | |||
| 5483e8e2c0 | |||
| e8949a2612 | |||
| db71a0b24d | |||
| d3d5422768 | |||
| 5185f753e5 |
@@ -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
|
||||
|
||||
|
||||
@@ -408,9 +408,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/iidx/mf_wrappers.cpp
|
||||
games/sdvx/bi2x_hook.cpp
|
||||
games/sdvx/sdvx.cpp
|
||||
games/sdvx/sdvx_live2d.cpp
|
||||
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
|
||||
@@ -430,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
|
||||
@@ -529,6 +532,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
||||
@@ -605,12 +609,18 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.cpp
|
||||
overlay/windows/midi.cpp
|
||||
overlay/windows/obs.cpp
|
||||
overlay/windows/obs_websocket.cpp
|
||||
overlay/windows/patch_manager.cpp
|
||||
overlay/windows/popn_sub.cpp
|
||||
overlay/windows/wnd_manager.cpp
|
||||
|
||||
# external
|
||||
external/easywsclient/easywsclient.cpp
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
rawinput/touch.cpp
|
||||
@@ -637,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
|
||||
|
||||
@@ -671,6 +682,33 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
|
||||
|
||||
# guard against statically importing DLLs that must always be loaded dynamically:
|
||||
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
|
||||
# static import loads the system copy at startup and preempts the override
|
||||
# (issue #779).
|
||||
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
|
||||
# Unity games.
|
||||
# the check runs objdump on each produced binary and fails the build if any
|
||||
# forbidden DLL is imported.
|
||||
set(SPICE_FORBIDDEN_STATIC_IMPORTS
|
||||
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
|
||||
mf.dll mfplat.dll mfreadwrite.dll)
|
||||
|
||||
function(spice_guard_dll_imports target)
|
||||
if(MSVC)
|
||||
# objdump-based parsing assumes a GNU/LLVM toolchain
|
||||
return()
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DOBJDUMP=${CMAKE_OBJDUMP}
|
||||
-DTARGET_FILE=$<TARGET_FILE:${target}>
|
||||
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
|
||||
VERBATIM
|
||||
COMMENT "Checking ${target} for forbidden static DLL imports")
|
||||
endfunction()
|
||||
|
||||
# spice.exe / spice_laa.exe shared objects
|
||||
###########################################
|
||||
# spice.exe and spice_laa.exe are compiled identically; the only difference is
|
||||
@@ -955,3 +993,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
|
||||
|
||||
# forbidden static DLL import guard
|
||||
###################################
|
||||
# apply the check to every executable and DLL produced by this project.
|
||||
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(spice_target IN LISTS spice_all_targets)
|
||||
get_target_property(spice_target_type ${spice_target} TYPE)
|
||||
if(spice_target_type STREQUAL "EXECUTABLE"
|
||||
OR spice_target_type STREQUAL "SHARED_LIBRARY"
|
||||
OR spice_target_type STREQUAL "MODULE_LIBRARY")
|
||||
spice_guard_dll_imports(${spice_target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <d3d9.h>
|
||||
|
||||
#include "overlay/overlay.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace cfg {
|
||||
@@ -35,7 +36,26 @@ namespace cfg {
|
||||
return false;
|
||||
}
|
||||
|
||||
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
|
||||
// load d3d9.dll dynamically rather than linking against it statically.
|
||||
// a static import would force the system d3d9.dll to load at process
|
||||
// startup for the main spice executable too (this file is shared with
|
||||
// spice.exe), which loads system32\d3d9.dll before the modules directory
|
||||
// is added to the DLL search path - preventing a user-supplied DXVK
|
||||
// d3d9.dll in modules from ever loading for the game.
|
||||
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
|
||||
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
|
||||
if (d3d9_module == nullptr) {
|
||||
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
|
||||
return false;
|
||||
}
|
||||
auto Direct3DCreate9_fn =
|
||||
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
|
||||
if (Direct3DCreate9_fn == nullptr) {
|
||||
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
|
||||
return false;
|
||||
}
|
||||
|
||||
IDirect3D9 *d3d = Direct3DCreate9_fn(D3D_SDK_VERSION);
|
||||
if (d3d == nullptr) {
|
||||
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
||||
return false;
|
||||
|
||||
@@ -21,6 +21,8 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
|
||||
static std::string WINDOW_TITLE;
|
||||
static int WINDOW_SIZE_X = 800;
|
||||
static int WINDOW_SIZE_Y = 600;
|
||||
static const int WINDOW_MIN_SIZE_X = 540;
|
||||
static const int WINDOW_MIN_SIZE_Y = 300;
|
||||
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
|
||||
|
||||
static const UINT_PTR RENDER_TIMER_ID = 1;
|
||||
@@ -85,6 +87,23 @@ static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
|
||||
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
// computes the top-left position that centers a window of the given size on
|
||||
// the primary monitor's work area (the desktop minus the taskbar); falls back
|
||||
// to (0, 0) if the monitor info can't be queried.
|
||||
static POINT center_on_primary_monitor(int width, int height) {
|
||||
POINT pos = { 0, 0 };
|
||||
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
|
||||
MONITORINFO mi {};
|
||||
mi.cbSize = sizeof(mi);
|
||||
if (GetMonitorInfo(mon, &mi)) {
|
||||
const int work_w = mi.rcWork.right - mi.rcWork.left;
|
||||
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
|
||||
pos.x = mi.rcWork.left + (work_w - width) / 2;
|
||||
pos.y = mi.rcWork.top + (work_h - height) / 2;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
|
||||
// clamped to a sane range. Falls back to 60 if detection fails or the value
|
||||
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
|
||||
@@ -243,8 +262,10 @@ cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
||||
|
||||
void cfg::ConfiguratorWindow::run() {
|
||||
|
||||
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
|
||||
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
||||
|
||||
// show window
|
||||
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
||||
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
||||
UpdateWindow(this->hWnd);
|
||||
|
||||
@@ -290,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_GETMINMAXINFO: {
|
||||
|
||||
// enforce a minimum window size so the UI can't be shrunk to a
|
||||
// point where the tabs/controls become unusable. The minimum is
|
||||
// expressed in client-area pixels and converted to a full window
|
||||
// size that accounts for the current frame/title-bar style.
|
||||
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
|
||||
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
|
||||
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
|
||||
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
|
||||
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
|
||||
mmi->ptMinTrackSize.x = rc.right - rc.left;
|
||||
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_CREATE: {
|
||||
|
||||
// set user data of window to class pointer
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# fails the build if a PE binary statically imports a forbidden DLL.
|
||||
#
|
||||
# some DLLs must never end up in spice's static import table, for two reasons:
|
||||
#
|
||||
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
|
||||
# into the modules directory to replace the system one. a static import
|
||||
# forces the loader to load the SYSTEM copy at process startup - before the
|
||||
# modules directory is added to the DLL search path and before the game DLL
|
||||
# loads - so the user-supplied override never takes effect (see issue #779).
|
||||
#
|
||||
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
|
||||
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
|
||||
#
|
||||
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
|
||||
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
|
||||
#
|
||||
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
|
||||
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
|
||||
# TARGET_FILE - path to the PE binary to inspect
|
||||
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
|
||||
|
||||
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
|
||||
OUTPUT_VARIABLE dump_output
|
||||
RESULT_VARIABLE dump_result
|
||||
ERROR_VARIABLE dump_error)
|
||||
|
||||
if(NOT dump_result EQUAL 0)
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
|
||||
# statically imported DLL in their PE private-header dump.
|
||||
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
|
||||
|
||||
set(violations "")
|
||||
foreach(line IN LISTS dll_lines)
|
||||
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
|
||||
string(STRIP "${dll_name}" dll_name)
|
||||
string(TOLOWER "${dll_name}" dll_name_lower)
|
||||
if(dll_name_lower IN_LIST FORBIDDEN)
|
||||
list(APPEND violations "${dll_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(violations)
|
||||
list(REMOVE_DUPLICATES violations)
|
||||
string(REPLACE ";" ", " violations_str "${violations}")
|
||||
message(FATAL_ERROR
|
||||
"static DLL import check FAILED for ${TARGET_FILE}\n"
|
||||
" forbidden static imports found: ${violations_str}\n"
|
||||
"\n"
|
||||
" these DLLs must never be statically imported by spice:\n"
|
||||
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
|
||||
" system copy at startup and preempts the modules override (issue #779).\n"
|
||||
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
|
||||
" Unity games.\n"
|
||||
"\n"
|
||||
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
|
||||
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
|
||||
" call through the resolved function pointer.")
|
||||
endif()
|
||||
|
||||
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
// This code comes from:
|
||||
// https://github.com/dhbaird/easywsclient
|
||||
//
|
||||
// To get the latest version:
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||
|
||||
#include "easywsclient.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#pragma comment( lib, "ws2_32" )
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
typedef SOCKET socket_t;
|
||||
#ifndef _SSIZE_T_DEFINED
|
||||
typedef int ssize_t;
|
||||
#define _SSIZE_T_DEFINED
|
||||
#endif
|
||||
#ifndef _SOCKET_T_DEFINED
|
||||
typedef SOCKET socket_t;
|
||||
#define _SOCKET_T_DEFINED
|
||||
#endif
|
||||
#if defined(_MSC_VER) && !defined(snprintf)
|
||||
#define snprintf _snprintf_s
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#define socketerrno WSAGetLastError()
|
||||
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
|
||||
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <errno.h>
|
||||
typedef int socket_t;
|
||||
#ifndef INVALID_SOCKET
|
||||
#define INVALID_SOCKET (-1)
|
||||
#endif
|
||||
#ifndef SOCKET_ERROR
|
||||
#define SOCKET_ERROR (-1)
|
||||
#endif
|
||||
#define closesocket(s) ::close(s)
|
||||
#include <errno.h>
|
||||
#define socketerrno errno
|
||||
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
|
||||
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
// When false, easywsclient's diagnostics are suppressed. The host application
|
||||
// opts in by setting this flag (see obs_websocket.cpp).
|
||||
bool EASYWSCLIENT_LOGGING_ENABLED = false;
|
||||
|
||||
// Route easywsclient's diagnostic output through the project logger instead of
|
||||
// writing to stderr, without editing the upstream source lines below: these two
|
||||
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
|
||||
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
|
||||
static inline void easywsclient_logf(const char *fmt, ...) {
|
||||
if (!EASYWSCLIENT_LOGGING_ENABLED) {
|
||||
return;
|
||||
}
|
||||
char buf[1024];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
// trim trailing newline(s); the logger appends its own
|
||||
size_t len = strlen(buf);
|
||||
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
|
||||
buf[--len] = '\0';
|
||||
}
|
||||
log_misc("easywsclient", "{}", buf);
|
||||
}
|
||||
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
|
||||
#define fputs(str, stream) easywsclient_logf("%s", str)
|
||||
|
||||
using namespace easywsclient;
|
||||
|
||||
namespace { // private module-only namespace
|
||||
|
||||
socket_t hostname_connect(const std::string& hostname, int port) {
|
||||
struct addrinfo hints;
|
||||
struct addrinfo *result;
|
||||
struct addrinfo *p;
|
||||
int ret;
|
||||
socket_t sockfd = INVALID_SOCKET;
|
||||
char sport[16];
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
snprintf(sport, 16, "%d", port);
|
||||
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
|
||||
{
|
||||
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
|
||||
return 1;
|
||||
}
|
||||
for(p = result; p != NULL; p = p->ai_next)
|
||||
{
|
||||
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||
if (sockfd == INVALID_SOCKET) { continue; }
|
||||
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
|
||||
break;
|
||||
}
|
||||
closesocket(sockfd);
|
||||
sockfd = INVALID_SOCKET;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
return sockfd;
|
||||
}
|
||||
|
||||
|
||||
class _DummyWebSocket : public easywsclient::WebSocket
|
||||
{
|
||||
public:
|
||||
void poll(int timeout) { }
|
||||
void send(const std::string& message) { }
|
||||
void sendBinary(const std::string& message) { }
|
||||
void sendBinary(const std::vector<uint8_t>& message) { }
|
||||
void sendPing() { }
|
||||
void close() { }
|
||||
readyStateValues getReadyState() const { return CLOSED; }
|
||||
void _dispatch(Callback_Imp & callable) { }
|
||||
void _dispatchBinary(BytesCallback_Imp& callable) { }
|
||||
};
|
||||
|
||||
|
||||
class _RealWebSocket : public easywsclient::WebSocket
|
||||
{
|
||||
public:
|
||||
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
|
||||
//
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
// +-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||
// |I|S|S|S| (4) |A| (7) | (16/64) |
|
||||
// |N|V|V|V| |S| | (if payload len==126/127) |
|
||||
// | |1|2|3| |K| | |
|
||||
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
||||
// | Extended payload length continued, if payload len == 127 |
|
||||
// + - - - - - - - - - - - - - - - +-------------------------------+
|
||||
// | |Masking-key, if MASK set to 1 |
|
||||
// +-------------------------------+-------------------------------+
|
||||
// | Masking-key (continued) | Payload Data |
|
||||
// +-------------------------------- - - - - - - - - - - - - - - - +
|
||||
// : Payload Data continued ... :
|
||||
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
||||
// | Payload Data continued ... |
|
||||
// +---------------------------------------------------------------+
|
||||
|
||||
struct wsheader_type {
|
||||
unsigned header_size;
|
||||
bool fin;
|
||||
bool mask;
|
||||
enum opcode_type {
|
||||
CONTINUATION = 0x0,
|
||||
TEXT_FRAME = 0x1,
|
||||
BINARY_FRAME = 0x2,
|
||||
CLOSE = 8,
|
||||
PING = 9,
|
||||
PONG = 0xa,
|
||||
} opcode;
|
||||
int N0;
|
||||
uint64_t N;
|
||||
uint8_t masking_key[4];
|
||||
};
|
||||
|
||||
std::vector<uint8_t> rxbuf;
|
||||
std::vector<uint8_t> txbuf;
|
||||
std::vector<uint8_t> receivedData;
|
||||
|
||||
socket_t sockfd;
|
||||
readyStateValues readyState;
|
||||
bool useMask;
|
||||
bool isRxBad;
|
||||
|
||||
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
|
||||
}
|
||||
|
||||
readyStateValues getReadyState() const {
|
||||
return readyState;
|
||||
}
|
||||
|
||||
void poll(int timeout) { // timeout in milliseconds
|
||||
if (readyState == CLOSED) {
|
||||
if (timeout > 0) {
|
||||
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||
select(0, NULL, NULL, NULL, &tv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeout != 0) {
|
||||
fd_set rfds;
|
||||
fd_set wfds;
|
||||
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||
FD_ZERO(&rfds);
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(sockfd, &rfds);
|
||||
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
|
||||
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
|
||||
}
|
||||
while (true) {
|
||||
// FD_ISSET(0, &rfds) will be true
|
||||
int N = rxbuf.size();
|
||||
ssize_t ret;
|
||||
rxbuf.resize(N + 1500);
|
||||
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
|
||||
if (false) { }
|
||||
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||
rxbuf.resize(N);
|
||||
break;
|
||||
}
|
||||
else if (ret <= 0) {
|
||||
rxbuf.resize(N);
|
||||
closesocket(sockfd);
|
||||
readyState = CLOSED;
|
||||
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
rxbuf.resize(N + ret);
|
||||
}
|
||||
}
|
||||
while (txbuf.size()) {
|
||||
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
|
||||
if (false) { } // ??
|
||||
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||
break;
|
||||
}
|
||||
else if (ret <= 0) {
|
||||
closesocket(sockfd);
|
||||
readyState = CLOSED;
|
||||
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
|
||||
}
|
||||
}
|
||||
if (!txbuf.size() && readyState == CLOSING) {
|
||||
closesocket(sockfd);
|
||||
readyState = CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void _dispatch(Callback_Imp & callable) {
|
||||
struct CallbackAdapter : public BytesCallback_Imp
|
||||
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
|
||||
{
|
||||
Callback_Imp& callable;
|
||||
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
|
||||
void operator()(const std::vector<uint8_t>& message) {
|
||||
std::string stringMessage(message.begin(), message.end());
|
||||
callable(stringMessage);
|
||||
}
|
||||
};
|
||||
CallbackAdapter bytesCallback(callable);
|
||||
_dispatchBinary(bytesCallback);
|
||||
}
|
||||
|
||||
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
|
||||
// TODO: consider acquiring a lock on rxbuf...
|
||||
while (true) {
|
||||
wsheader_type ws;
|
||||
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
|
||||
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
|
||||
ws.fin = (data[0] & 0x80) == 0x80;
|
||||
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
|
||||
ws.mask = (data[1] & 0x80) == 0x80;
|
||||
ws.N0 = (data[1] & 0x7f);
|
||||
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
|
||||
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
|
||||
int i = 0;
|
||||
if (ws.N0 < 126) {
|
||||
ws.N = ws.N0;
|
||||
i = 2;
|
||||
}
|
||||
else if (ws.N0 == 126) {
|
||||
ws.N = 0;
|
||||
ws.N |= ((uint64_t) data[2]) << 8;
|
||||
ws.N |= ((uint64_t) data[3]) << 0;
|
||||
i = 4;
|
||||
}
|
||||
else if (ws.N0 == 127) {
|
||||
ws.N = 0;
|
||||
ws.N |= ((uint64_t) data[2]) << 56;
|
||||
ws.N |= ((uint64_t) data[3]) << 48;
|
||||
ws.N |= ((uint64_t) data[4]) << 40;
|
||||
ws.N |= ((uint64_t) data[5]) << 32;
|
||||
ws.N |= ((uint64_t) data[6]) << 24;
|
||||
ws.N |= ((uint64_t) data[7]) << 16;
|
||||
ws.N |= ((uint64_t) data[8]) << 8;
|
||||
ws.N |= ((uint64_t) data[9]) << 0;
|
||||
i = 10;
|
||||
}
|
||||
if (ws.mask) {
|
||||
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
|
||||
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
|
||||
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
|
||||
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
|
||||
}
|
||||
else {
|
||||
ws.masking_key[0] = 0;
|
||||
ws.masking_key[1] = 0;
|
||||
ws.masking_key[2] = 0;
|
||||
ws.masking_key[3] = 0;
|
||||
}
|
||||
|
||||
// Note: The checks above should hopefully ensure this addition
|
||||
// cannot overflow:
|
||||
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
|
||||
|
||||
// We got a whole message, now do something with it:
|
||||
if (false) { }
|
||||
else if (
|
||||
ws.opcode == wsheader_type::TEXT_FRAME
|
||||
|| ws.opcode == wsheader_type::BINARY_FRAME
|
||||
|| ws.opcode == wsheader_type::CONTINUATION
|
||||
) {
|
||||
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
|
||||
if (ws.fin) {
|
||||
callable((const std::vector<uint8_t>) receivedData);
|
||||
receivedData.erase(receivedData.begin(), receivedData.end());
|
||||
std::vector<uint8_t> ().swap(receivedData);// free memory
|
||||
}
|
||||
}
|
||||
else if (ws.opcode == wsheader_type::PING) {
|
||||
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
|
||||
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
|
||||
}
|
||||
else if (ws.opcode == wsheader_type::PONG) { }
|
||||
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
|
||||
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
|
||||
|
||||
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
|
||||
}
|
||||
}
|
||||
|
||||
void sendPing() {
|
||||
std::string empty;
|
||||
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
|
||||
}
|
||||
|
||||
void send(const std::string& message) {
|
||||
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
|
||||
}
|
||||
|
||||
void sendBinary(const std::string& message) {
|
||||
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||
}
|
||||
|
||||
void sendBinary(const std::vector<uint8_t>& message) {
|
||||
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||
}
|
||||
|
||||
template<class Iterator>
|
||||
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
|
||||
// TODO:
|
||||
// Masking key should (must) be derived from a high quality random
|
||||
// number generator, to mitigate attacks on non-WebSocket friendly
|
||||
// middleware:
|
||||
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
|
||||
// TODO: consider acquiring a lock on txbuf...
|
||||
if (readyState == CLOSING || readyState == CLOSED) { return; }
|
||||
std::vector<uint8_t> header;
|
||||
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
|
||||
header[0] = 0x80 | type;
|
||||
if (false) { }
|
||||
else if (message_size < 126) {
|
||||
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
|
||||
if (useMask) {
|
||||
header[2] = masking_key[0];
|
||||
header[3] = masking_key[1];
|
||||
header[4] = masking_key[2];
|
||||
header[5] = masking_key[3];
|
||||
}
|
||||
}
|
||||
else if (message_size < 65536) {
|
||||
header[1] = 126 | (useMask ? 0x80 : 0);
|
||||
header[2] = (message_size >> 8) & 0xff;
|
||||
header[3] = (message_size >> 0) & 0xff;
|
||||
if (useMask) {
|
||||
header[4] = masking_key[0];
|
||||
header[5] = masking_key[1];
|
||||
header[6] = masking_key[2];
|
||||
header[7] = masking_key[3];
|
||||
}
|
||||
}
|
||||
else { // TODO: run coverage testing here
|
||||
header[1] = 127 | (useMask ? 0x80 : 0);
|
||||
header[2] = (message_size >> 56) & 0xff;
|
||||
header[3] = (message_size >> 48) & 0xff;
|
||||
header[4] = (message_size >> 40) & 0xff;
|
||||
header[5] = (message_size >> 32) & 0xff;
|
||||
header[6] = (message_size >> 24) & 0xff;
|
||||
header[7] = (message_size >> 16) & 0xff;
|
||||
header[8] = (message_size >> 8) & 0xff;
|
||||
header[9] = (message_size >> 0) & 0xff;
|
||||
if (useMask) {
|
||||
header[10] = masking_key[0];
|
||||
header[11] = masking_key[1];
|
||||
header[12] = masking_key[2];
|
||||
header[13] = masking_key[3];
|
||||
}
|
||||
}
|
||||
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
|
||||
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||
txbuf.insert(txbuf.end(), message_begin, message_end);
|
||||
if (useMask) {
|
||||
size_t message_offset = txbuf.size() - message_size;
|
||||
for (size_t i = 0; i != message_size; ++i) {
|
||||
txbuf[message_offset + i] ^= masking_key[i&0x3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
if(readyState == CLOSING || readyState == CLOSED) { return; }
|
||||
readyState = CLOSING;
|
||||
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
|
||||
std::vector<uint8_t> header(closeFrame, closeFrame+6);
|
||||
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
|
||||
char host[512];
|
||||
int port;
|
||||
char path[512];
|
||||
if (url.size() >= 512) {
|
||||
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
|
||||
return NULL;
|
||||
}
|
||||
if (origin.size() >= 200) {
|
||||
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
|
||||
return NULL;
|
||||
}
|
||||
if (false) { }
|
||||
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
|
||||
}
|
||||
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
|
||||
port = 80;
|
||||
}
|
||||
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
|
||||
path[0] = '\0';
|
||||
}
|
||||
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
|
||||
port = 80;
|
||||
path[0] = '\0';
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
|
||||
return NULL;
|
||||
}
|
||||
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
|
||||
socket_t sockfd = hostname_connect(host, port);
|
||||
if (sockfd == INVALID_SOCKET) {
|
||||
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
|
||||
return NULL;
|
||||
}
|
||||
{
|
||||
// XXX: this should be done non-blocking,
|
||||
char line[1024];
|
||||
int status;
|
||||
int i;
|
||||
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
|
||||
if (port == 80) {
|
||||
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
|
||||
}
|
||||
else {
|
||||
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
|
||||
}
|
||||
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||
if (!origin.empty()) {
|
||||
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
|
||||
}
|
||||
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||
line[i] = 0;
|
||||
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
|
||||
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
|
||||
// TODO: verify response headers,
|
||||
while (true) {
|
||||
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||
if (line[0] == '\r' && line[1] == '\n') { break; }
|
||||
}
|
||||
}
|
||||
int flag = 1;
|
||||
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
|
||||
#ifdef _WIN32
|
||||
u_long on = 1;
|
||||
ioctlsocket(sockfd, FIONBIO, &on);
|
||||
#else
|
||||
fcntl(sockfd, F_SETFL, O_NONBLOCK);
|
||||
#endif
|
||||
//fprintf(stderr, "Connected to: %s\n", url.c_str());
|
||||
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
|
||||
}
|
||||
|
||||
} // end of module-only namespace
|
||||
|
||||
|
||||
|
||||
namespace easywsclient {
|
||||
|
||||
WebSocket::pointer WebSocket::create_dummy() {
|
||||
static pointer dummy = pointer(new _DummyWebSocket);
|
||||
return dummy;
|
||||
}
|
||||
|
||||
|
||||
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
|
||||
return ::from_url(url, true, origin);
|
||||
}
|
||||
|
||||
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
|
||||
return ::from_url(url, false, origin);
|
||||
}
|
||||
|
||||
|
||||
} // namespace easywsclient
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
|
||||
// This code comes from:
|
||||
// https://github.com/dhbaird/easywsclient
|
||||
//
|
||||
// To get the latest version:
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace easywsclient {
|
||||
|
||||
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
|
||||
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
|
||||
|
||||
class WebSocket {
|
||||
public:
|
||||
typedef WebSocket * pointer;
|
||||
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
|
||||
|
||||
// Factories:
|
||||
static pointer create_dummy();
|
||||
static pointer from_url(const std::string& url, const std::string& origin = std::string());
|
||||
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
|
||||
|
||||
// Interfaces:
|
||||
virtual ~WebSocket() { }
|
||||
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
||||
virtual void send(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
|
||||
virtual void sendPing() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual readyStateValues getReadyState() const = 0;
|
||||
|
||||
template<class Callable>
|
||||
void dispatch(Callable callable)
|
||||
// For callbacks that accept a string argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public Callback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::string& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatch(callback);
|
||||
}
|
||||
|
||||
template<class Callable>
|
||||
void dispatchBinary(Callable callable)
|
||||
// For callbacks that accept a std::vector<uint8_t> argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public BytesCallback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::vector<uint8_t>& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatchBinary(callback);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void _dispatch(Callback_Imp& callable) = 0;
|
||||
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
|
||||
};
|
||||
|
||||
} // namespace easywsclient
|
||||
|
||||
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
|
||||
@@ -33,13 +33,14 @@ namespace games::ccj {
|
||||
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
|
||||
UINT cbSize) {
|
||||
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
|
||||
if (result == 0xFFFFFFFF)
|
||||
if (result == 0xFFFFFFFF) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (pRawInputDeviceList == NULL) {
|
||||
(*puiNumDevices)++;
|
||||
} else if (result < *puiNumDevices) {
|
||||
pRawInputDeviceList[result] = {fakeHandle, 0};
|
||||
pRawInputDeviceList[result] = { fakeHandle, 0 };
|
||||
result++;
|
||||
}
|
||||
|
||||
@@ -47,8 +48,9 @@ namespace games::ccj {
|
||||
}
|
||||
|
||||
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
|
||||
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME)
|
||||
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
|
||||
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
|
||||
}
|
||||
|
||||
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
|
||||
|
||||
@@ -68,23 +70,157 @@ namespace games::ccj {
|
||||
|
||||
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
|
||||
wchar_t buffer[256];
|
||||
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName))
|
||||
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
|
||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||
}
|
||||
|
||||
hWnd = _hWnd;
|
||||
wndProc = (WNDPROC)dwNewLong;
|
||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||
}
|
||||
|
||||
// compute the cursor wrap region in client coordinates. The cursor is confined to the
|
||||
// monitor, so if the window's client area extends past a screen edge (window larger than /
|
||||
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
|
||||
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
|
||||
// the left/top edges.
|
||||
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
|
||||
RECT bounds = client;
|
||||
|
||||
POINT origin = { 0, 0 };
|
||||
ClientToScreen(wnd, &origin);
|
||||
RECT clientScreen = {
|
||||
origin.x,
|
||||
origin.y,
|
||||
origin.x + client.right,
|
||||
origin.y + client.bottom
|
||||
};
|
||||
|
||||
MONITORINFO mi = {};
|
||||
mi.cbSize = sizeof(mi);
|
||||
RECT usable;
|
||||
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
|
||||
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
|
||||
bounds.left = usable.left - origin.x;
|
||||
bounds.top = usable.top - origin.y;
|
||||
bounds.right = usable.right - origin.x;
|
||||
bounds.bottom = usable.bottom - origin.y;
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
|
||||
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
|
||||
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
|
||||
static bool active = false;
|
||||
static bool lastState = false;
|
||||
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
|
||||
static const std::chrono::milliseconds debounceDuration(100);
|
||||
|
||||
const auto currentTime = std::chrono::steady_clock::now();
|
||||
const bool pressed = get_async_secondary_mouse();
|
||||
const bool focused = GetForegroundWindow() == hWnd;
|
||||
|
||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
||||
active = !active;
|
||||
lastModified = currentTime;
|
||||
}
|
||||
|
||||
const bool engaged = focused
|
||||
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
|
||||
if (!engaged) {
|
||||
if (lastState && !active) {
|
||||
lastState = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
POINT cursor;
|
||||
RECT client;
|
||||
GetClientRect(hWnd, &client);
|
||||
GetCursorPos(&cursor);
|
||||
ScreenToClient(hWnd, &cursor);
|
||||
|
||||
const RECT bounds = trackball_wrap_bounds(hWnd, client);
|
||||
|
||||
static int lastX = cursor.x;
|
||||
static int lastY = cursor.y;
|
||||
if (!lastState) {
|
||||
lastX = cursor.x;
|
||||
lastY = cursor.y;
|
||||
lastState = true;
|
||||
}
|
||||
|
||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||
|
||||
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
|
||||
// can keep spinning past the screen edge.
|
||||
bool updateCursor = false;
|
||||
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
|
||||
if (value <= lo) {
|
||||
updateCursor = true;
|
||||
return hi - 5;
|
||||
}
|
||||
if (value >= hi - 1) {
|
||||
updateCursor = true;
|
||||
return lo + 5;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
|
||||
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
|
||||
|
||||
lastX = cursor.x;
|
||||
lastY = cursor.y;
|
||||
|
||||
if (updateCursor) {
|
||||
ClientToScreen(hWnd, &cursor);
|
||||
SetCursorPos(cursor.x, cursor.y);
|
||||
}
|
||||
}
|
||||
|
||||
// drive the trackball from the configured analog axes / direction buttons.
|
||||
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
|
||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||
|
||||
auto &analogs = get_analogs();
|
||||
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
||||
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
||||
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
||||
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
||||
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
||||
}
|
||||
|
||||
auto &buttons = get_buttons();
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
|
||||
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
|
||||
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
|
||||
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
|
||||
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
||||
}
|
||||
}
|
||||
|
||||
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
|
||||
if (hRawInput != fakeHandle)
|
||||
if (hRawInput != fakeHandle) {
|
||||
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
|
||||
}
|
||||
|
||||
if (pData == NULL) {
|
||||
if (uiCommand == RID_HEADER)
|
||||
if (uiCommand == RID_HEADER) {
|
||||
*pcbSize = sizeof(RAWINPUTHEADER);
|
||||
else
|
||||
} else {
|
||||
*pcbSize = sizeof(RAWINPUT);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -107,92 +243,9 @@ namespace games::ccj {
|
||||
RAWMOUSE rawMouse {};
|
||||
|
||||
if (MOUSE_TRACKBALL) {
|
||||
static bool active = false;
|
||||
static bool lastState = false;
|
||||
|
||||
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
|
||||
static std::chrono::milliseconds debounceDuration(100);
|
||||
auto currentTime = std::chrono::steady_clock::now();
|
||||
bool pressed = get_async_secondary_mouse();
|
||||
bool focused = GetForegroundWindow() == hWnd;
|
||||
|
||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
||||
active = !active;
|
||||
lastModified = currentTime;
|
||||
}
|
||||
|
||||
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
|
||||
POINT cursor;
|
||||
RECT client;
|
||||
|
||||
GetClientRect(hWnd, &client);
|
||||
int sx = client.right - client.left;
|
||||
int sy = client.bottom - client.top;
|
||||
|
||||
GetCursorPos(&cursor);
|
||||
ScreenToClient(hWnd, &cursor);
|
||||
|
||||
static int lastX = cursor.x;
|
||||
static int lastY = cursor.y;
|
||||
|
||||
if (!lastState) {
|
||||
lastX = cursor.x;
|
||||
lastY = cursor.y;
|
||||
lastState = true;
|
||||
}
|
||||
|
||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||
|
||||
bool updateCursor = false;
|
||||
|
||||
if (cursor.x <= 0) {
|
||||
updateCursor = true;
|
||||
cursor.x = sx - 5;
|
||||
} else if (cursor.x >= sx - 1) {
|
||||
updateCursor = true;
|
||||
cursor.x = 5;
|
||||
}
|
||||
|
||||
if (cursor.y <= 0) {
|
||||
updateCursor = true;
|
||||
cursor.y = sy - 5;
|
||||
} else if (cursor.y >= sy - 1) {
|
||||
updateCursor = true;
|
||||
cursor.y = 5;
|
||||
}
|
||||
|
||||
lastX = cursor.x;
|
||||
lastY = cursor.y;
|
||||
|
||||
if (updateCursor) {
|
||||
ClientToScreen(hWnd, &cursor);
|
||||
SetCursorPos(cursor.x, cursor.y);
|
||||
}
|
||||
} else if (lastState && !active) {
|
||||
lastState = false;
|
||||
}
|
||||
trackball_mouse_input(rawMouse);
|
||||
} else {
|
||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||
|
||||
auto &analogs = get_analogs();
|
||||
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
||||
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
||||
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
||||
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
||||
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
||||
}
|
||||
|
||||
auto &buttons = get_buttons();
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
|
||||
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
|
||||
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
|
||||
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
|
||||
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
||||
trackball_mapped_input(rawMouse);
|
||||
}
|
||||
|
||||
*((RAWINPUT*)pData) = { header, { rawMouse } };
|
||||
@@ -224,9 +277,8 @@ namespace games::ccj {
|
||||
static bool initialized = false;
|
||||
if (initialized) {
|
||||
return;
|
||||
} else {
|
||||
initialized = true;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
// announce
|
||||
log_info("trackball", "init");
|
||||
@@ -246,8 +298,6 @@ namespace games::ccj {
|
||||
}
|
||||
|
||||
void trackball_thread_start() {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
tbThreadRunning = true;
|
||||
|
||||
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
||||
@@ -259,8 +309,9 @@ namespace games::ccj {
|
||||
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
||||
}
|
||||
|
||||
if (!tbThreadRunning)
|
||||
if (!tbThreadRunning) {
|
||||
break;
|
||||
}
|
||||
|
||||
timer.sleep(10);
|
||||
}
|
||||
@@ -269,8 +320,9 @@ namespace games::ccj {
|
||||
|
||||
void trackball_thread_stop() {
|
||||
tbThreadRunning = false;
|
||||
if (tbThread)
|
||||
if (tbThread) {
|
||||
tbThread->join();
|
||||
}
|
||||
|
||||
log_info("trackball", "thread stop");
|
||||
|
||||
|
||||
@@ -474,6 +474,8 @@ namespace games {
|
||||
vkey_defaults.push_back(VK_F12);
|
||||
names.emplace_back("Toggle Camera Control");
|
||||
vkey_defaults.push_back(0xFF);
|
||||
names.emplace_back("Toggle OBS Control");
|
||||
vkey_defaults.push_back(0xFF);
|
||||
names.emplace_back("Player 1 PIN Macro");
|
||||
vkey_defaults.push_back(0xFF);
|
||||
names.emplace_back("Player 2 PIN Macro");
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace games {
|
||||
ToggleScreenResize,
|
||||
ToggleFps,
|
||||
ToggleCameraControl,
|
||||
ToggleOBSControl,
|
||||
TriggerPinMacroP1,
|
||||
TriggerPinMacroP2,
|
||||
ScreenResize,
|
||||
|
||||
+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;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "sdvx_live2d.h"
|
||||
|
||||
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||
// compiled out of 32-bit builds.
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::sdvx {
|
||||
|
||||
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
|
||||
//
|
||||
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
|
||||
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
|
||||
// Live2D rendering); we watch those log lines and keep the shared flag
|
||||
// the d3d9 backend reads up to date. the hook never alters the log output
|
||||
// (always returns false).
|
||||
static bool live2d_scene_log_hook(
|
||||
void *user, const std::string &data, logger::Style style, std::string &out) {
|
||||
// any of these scenes counts as in-song gameplay (different play modes)
|
||||
static const char *const gameplay_scenes[] = {
|
||||
"in ALTERNATIVE_GAME_SCENE",
|
||||
"in MEGAMIX_GAME_SCENE",
|
||||
"in MEGAMIX_BATTLE",
|
||||
"in BATTLE_GAME_SCENE",
|
||||
"in AUTOMATION_GAME_SCENE",
|
||||
"in ARENA_GAME_SCENE",
|
||||
};
|
||||
bool in_gameplay_scene = false;
|
||||
for (const auto *scene : gameplay_scenes) {
|
||||
if (data.find(scene) != std::string::npos) {
|
||||
in_gameplay_scene = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!in_gameplay_scene) {
|
||||
return false;
|
||||
}
|
||||
// note: log messages here must NOT contain any matched scene token, else
|
||||
// this hook would re-enter itself when the message is pushed.
|
||||
if (data.find("I:Attach: in ") != std::string::npos) {
|
||||
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
|
||||
log_info("sdvx", "Live2D skip: entering gameplay");
|
||||
}
|
||||
} else if (data.find("I:Detach: in ") != std::string::npos) {
|
||||
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
|
||||
log_info("sdvx", "Live2D skip: leaving gameplay");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void live2d_scene_detection_init() {
|
||||
static bool installed = false;
|
||||
if (installed) {
|
||||
return;
|
||||
}
|
||||
installed = true;
|
||||
// the logger's hook list is a persistent static, so registering here is
|
||||
// safe even though this runs before logger::start(). we intentionally do
|
||||
// NOT log a confirmation now: at this point the log file isn't open yet
|
||||
// and the message would be dropped. the entering/leaving-gameplay lines
|
||||
// above provide runtime confirmation once the logger is running.
|
||||
logger::hook_add(live2d_scene_log_hook, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SPICE64
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::sdvx {
|
||||
|
||||
#ifdef SPICE64
|
||||
// installs the Live2D in-game scene-detection log hook used by the
|
||||
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
|
||||
// be attached, so it can be enabled purely from the launcher option.
|
||||
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
|
||||
// of 32-bit builds.
|
||||
void live2d_scene_detection_init();
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1273,9 +1273,11 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
||||
wintouchemu::update();
|
||||
|
||||
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
|
||||
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen
|
||||
// early versions of popn HC needs this as well, otherwise the subscreen doesn't update at all
|
||||
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW || games::popn::is_pikapika_model()) {
|
||||
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen)
|
||||
//
|
||||
// early versions of popn HC needs this as well, but not on by default as it can cause
|
||||
// graphical glitches on some GPUs
|
||||
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
|
||||
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#include "d3d9_device.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <climits>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
#include "cfg/screen_resize.h"
|
||||
|
||||
#include "d3d9_backend.h"
|
||||
#include "d3d9_live2d.h"
|
||||
#include "d3d9_texture.h"
|
||||
|
||||
#ifndef SPICE64
|
||||
@@ -1301,6 +1306,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitive(
|
||||
UINT PrimitiveCount)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||
return D3D_OK;
|
||||
}
|
||||
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
|
||||
}
|
||||
|
||||
@@ -1313,6 +1321,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitive(
|
||||
UINT PrimitiveCount)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||
return D3D_OK;
|
||||
}
|
||||
CHECK_RESULT(pReal->DrawIndexedPrimitive(
|
||||
PrimitiveType, BaseVertexIndex,
|
||||
MinVertexIndex, NumVertices,
|
||||
@@ -1328,6 +1339,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitiveUP(
|
||||
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
|
||||
PrimitiveType, PrimitiveCount,
|
||||
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
|
||||
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||
return D3D_OK;
|
||||
}
|
||||
CHECK_RESULT(pReal->DrawPrimitiveUP(
|
||||
PrimitiveType, PrimitiveCount,
|
||||
pVertexStreamZeroData, VertexStreamZeroStride));
|
||||
@@ -1344,6 +1358,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitiveUP(
|
||||
UINT VertexStreamZeroStride)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||
return D3D_OK;
|
||||
}
|
||||
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
|
||||
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
|
||||
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
|
||||
@@ -1403,7 +1420,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateVertexShader(
|
||||
IDirect3DVertexShader9 **ppShader)
|
||||
{
|
||||
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
|
||||
CHECK_RESULT(pReal->CreateVertexShader(pFunction, ppShader));
|
||||
HRESULT ret = pReal->CreateVertexShader(pFunction, ppShader);
|
||||
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||
d3d9_live2d::on_create_vertex_shader(*ppShader, pFunction);
|
||||
}
|
||||
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
||||
@@ -1411,6 +1435,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
||||
{
|
||||
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
|
||||
|
||||
d3d9_live2d::on_set_vertex_shader(pShader);
|
||||
|
||||
#ifndef SPICE64
|
||||
|
||||
// diagonal line fix
|
||||
@@ -1557,13 +1583,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreatePixelShader(
|
||||
IDirect3DPixelShader9 **ppShader)
|
||||
{
|
||||
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
|
||||
CHECK_RESULT(pReal->CreatePixelShader(pFunction, ppShader));
|
||||
HRESULT ret = pReal->CreatePixelShader(pFunction, ppShader);
|
||||
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||
d3d9_live2d::on_create_pixel_shader(*ppShader, pFunction);
|
||||
}
|
||||
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
|
||||
IDirect3DPixelShader9 *pShader)
|
||||
{
|
||||
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
|
||||
d3d9_live2d::on_set_pixel_shader(pShader);
|
||||
CHECK_RESULT(pReal->SetPixelShader(pShader));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "d3d9_live2d.h"
|
||||
|
||||
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
|
||||
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
// how the Live2D draw filtering works
|
||||
// ------------------------------------
|
||||
// SDVX draws its Live2D characters with a small, fixed set of pixel and
|
||||
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at
|
||||
// the exact moment the game issues a draw call. the d3d9 device hooks feed three
|
||||
// kinds of events into this module:
|
||||
//
|
||||
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
|
||||
// the game compiles its shaders once at load. we can't trust the shader
|
||||
// *object pointer* to identify a shader (it's just a heap address that
|
||||
// varies per run and can be recycled), so instead we hash the shader's
|
||||
// D3D9 *bytecode* - that fingerprint is stable across runs because the
|
||||
// game ships the same shaders. if the hash matches a known Live2D shader
|
||||
// we remember that object pointer in g_live2d_shaders.
|
||||
//
|
||||
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
|
||||
// whenever the game binds a shader we look it up in that set once and cache
|
||||
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
|
||||
// far less often than draws, so this is where the lookup cost lives.
|
||||
//
|
||||
// 3. draw call (should_skip_draw, called from every Draw* hook)
|
||||
// the per-draw question "is this a Live2D draw?" is then just reading those
|
||||
// two cached bools - no hashing, no map lookups. if the skip is currently
|
||||
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is
|
||||
// Live2D, the Draw* hook drops the call instead of forwarding it.
|
||||
//
|
||||
// everything is gated on the feature being enabled (mode != Off); when it's Off
|
||||
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a
|
||||
// device is single-threaded, so none of this state needs locking.
|
||||
|
||||
namespace {
|
||||
|
||||
// shader state is tracked whenever the feature might act (mode != Off) so the
|
||||
// known-shader set is populated before a song starts. when Off, every entry
|
||||
// point is a single cheap branch.
|
||||
bool tracking_enabled() {
|
||||
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
|
||||
}
|
||||
|
||||
// the set of shader objects (pixel or vertex) whose bytecode matched a known
|
||||
// Live2D fingerprint. only matching shaders are stored, so this stays tiny.
|
||||
std::unordered_set<void *> g_live2d_shaders;
|
||||
|
||||
// whether the currently-bound shaders are known Live2D shaders. cached at set
|
||||
// time so the per-draw check is just two bool reads.
|
||||
bool g_cur_ps_is_live2d = false;
|
||||
bool g_cur_vs_is_live2d = false;
|
||||
|
||||
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
|
||||
uint64_t bytecode_hash(const DWORD *func) {
|
||||
if (func == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const DWORD *p = func;
|
||||
const DWORD *cap = func + 65536; // safety bound
|
||||
while (p < cap && *p != 0x0000FFFF) {
|
||||
p++;
|
||||
}
|
||||
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
const auto *bytes = reinterpret_cast<const uint8_t *>(func);
|
||||
for (size_t i = 0; i < n_bytes; i++) {
|
||||
h ^= bytes[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
|
||||
// across runs because the game ships fixed shaders. the two sets are disjoint so
|
||||
// a single shader can be classified by its own hash alone.
|
||||
bool hash_is_live2d(uint64_t hash) {
|
||||
switch (hash) {
|
||||
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
|
||||
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
|
||||
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
|
||||
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
|
||||
case 0xe9cf898c331e2a51ULL: // vertex
|
||||
case 0x94dc84e7b7c0f437ULL: // vertex
|
||||
case 0xc872937c5cc04309ULL: // vertex
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// classify a shader at creation time and record it if it is Live2D. erasing on a
|
||||
// miss keeps the set correct if the runtime reuses a freed shader pointer.
|
||||
void classify_shader(void *shader, const DWORD *func) {
|
||||
if (hash_is_live2d(bytecode_hash(func))) {
|
||||
g_live2d_shaders.insert(shader);
|
||||
} else {
|
||||
g_live2d_shaders.erase(shader);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace d3d9_live2d {
|
||||
|
||||
// stage 1: fingerprint each shader as the game creates it
|
||||
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
|
||||
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||
classify_shader(shader, func);
|
||||
}
|
||||
}
|
||||
|
||||
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
|
||||
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||
classify_shader(shader, func);
|
||||
}
|
||||
}
|
||||
|
||||
// stage 2: remember whether the just-bound shader is a Live2D one
|
||||
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
|
||||
if (tracking_enabled()) [[unlikely]] {
|
||||
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
|
||||
if (tracking_enabled()) [[unlikely]] {
|
||||
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// stage 3: drop the draw if the skip is active and a Live2D shader is bound
|
||||
bool should_skip_draw() {
|
||||
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
|
||||
}
|
||||
|
||||
} // namespace d3d9_live2d
|
||||
|
||||
#endif // SPICE64
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d9.h>
|
||||
|
||||
// SDVX Live2D draw-skip support for the D3D9 backend.
|
||||
//
|
||||
// SDVX renders its Live2D navigator / in-song character through a fixed set of
|
||||
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
|
||||
// the matching draw calls are dropped to save GPU. shaders are identified by a
|
||||
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
|
||||
// does not). the hashes were captured with the draw-call fingerprinting tool.
|
||||
//
|
||||
// every entry point is a no-op unless the feature is enabled (mode != Off), and
|
||||
// d3d9 rendering for a device is single-threaded, so none of this needs locking.
|
||||
namespace d3d9_live2d {
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
// record a shader's bytecode fingerprint at creation time
|
||||
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
|
||||
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
|
||||
|
||||
// remember the currently-bound shaders
|
||||
void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
|
||||
void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
|
||||
|
||||
// true if the current draw call should be dropped (skip active AND the bound
|
||||
// shaders identify it as SDVX Live2D)
|
||||
bool should_skip_draw();
|
||||
|
||||
#else // !SPICE64
|
||||
|
||||
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
|
||||
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
|
||||
// call sites.
|
||||
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
|
||||
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
|
||||
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
|
||||
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
|
||||
inline bool should_skip_draw() { return false; }
|
||||
|
||||
#endif // SPICE64
|
||||
}
|
||||
@@ -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"
|
||||
@@ -86,6 +86,10 @@ static bool monitor_layout_needs_reset = false;
|
||||
bool GRAPHICS_CAPTURE_CURSOR = false;
|
||||
bool GRAPHICS_LOG_HRESULT = false;
|
||||
bool GRAPHICS_SDVX_FORCE_720 = false;
|
||||
#ifdef SPICE64
|
||||
SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Off;
|
||||
std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY = false;
|
||||
#endif // SPICE64
|
||||
bool GRAPHICS_SHOW_CURSOR = false;
|
||||
bool GRAPHICS_WINDOWED = false;
|
||||
std::vector<HWND> GRAPHICS_WINDOWS;
|
||||
@@ -623,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 {}, {}",
|
||||
@@ -718,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
@@ -27,6 +28,33 @@ enum graphics_dx9on12_state {
|
||||
DX9ON12_FORCE_ON,
|
||||
};
|
||||
|
||||
// SDVX Live2D suppression policy. the mode comes from the launcher
|
||||
// option; the in-gameplay flag is maintained by the SDVX scene-detection hook in
|
||||
// games/sdvx (which does not require the SDVX game module to be enabled). the
|
||||
// d3d9 backend reads graphics_sdvx_live2d_should_skip() on every draw.
|
||||
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||
// compiled out of 32-bit builds.
|
||||
#ifdef SPICE64
|
||||
enum class SdvxLive2dMode {
|
||||
Off, // leave Live2D untouched (default)
|
||||
Always, // always skip Live2D draws (also removes the menu navigator)
|
||||
InGame, // skip Live2D draws only during a song
|
||||
};
|
||||
extern SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE;
|
||||
extern std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY;
|
||||
|
||||
inline bool graphics_sdvx_live2d_should_skip() {
|
||||
switch (GRAPHICS_SDVX_LIVE2D_MODE) {
|
||||
case SdvxLive2dMode::Always:
|
||||
return true;
|
||||
case SdvxLive2dMode::InGame:
|
||||
return GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.load(std::memory_order_relaxed);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif // SPICE64
|
||||
|
||||
// flag settings
|
||||
extern bool GRAPHICS_CAPTURE_CURSOR;
|
||||
extern bool GRAPHICS_LOG_HRESULT;
|
||||
|
||||
@@ -49,10 +49,12 @@
|
||||
#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"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
#include "games/sdvx/sdvx_live2d.h"
|
||||
#include "games/shared/printer.h"
|
||||
#include "games/silentscope/silentscope.h"
|
||||
#include "games/mfc/mfc.h"
|
||||
@@ -102,6 +104,7 @@
|
||||
#include "overlay/notifications.h"
|
||||
#include "overlay/windows/patch_manager.h"
|
||||
#include "overlay/windows/iidx_seg.h"
|
||||
#include "overlay/windows/obs.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "reader/reader.h"
|
||||
@@ -643,6 +646,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
||||
games::popn::NATIVE_TOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||
SUBSCREEN_FORCE_REDRAW = true;
|
||||
}
|
||||
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
|
||||
attach_mga = true;
|
||||
}
|
||||
@@ -1042,9 +1048,32 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::SDVXForce720p].value_bool()) {
|
||||
GRAPHICS_SDVX_FORCE_720 = true;
|
||||
}
|
||||
#ifdef SPICE64
|
||||
// only the Live2D-capable SDVX versions are 64-bit, so this whole feature is
|
||||
// gated out of 32-bit builds and only armed for SDVX (model KFC).
|
||||
if (avs::game::is_model("KFC")) {
|
||||
auto live2d = options[launcher::Options::SDVXDisableLive2D].value_text();
|
||||
if (live2d == "always") {
|
||||
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Always;
|
||||
} else if (live2d == "ingame") {
|
||||
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::InGame;
|
||||
// scene detection runs independently of the SDVX game module
|
||||
games::sdvx::live2d_scene_detection_init();
|
||||
}
|
||||
}
|
||||
#endif // SPICE64
|
||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||
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;
|
||||
@@ -1086,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) {
|
||||
@@ -1286,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();
|
||||
}
|
||||
@@ -1434,6 +1481,26 @@ int main_implementation(int argc, char *argv[]) {
|
||||
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
||||
}
|
||||
|
||||
// OBS WebSocket overlay settings
|
||||
if (options[launcher::Options::OBSWebSocketEnabled].value_bool()) {
|
||||
overlay::windows::OBS_CONTROL_ENABLED = true;
|
||||
}
|
||||
if (options[launcher::Options::OBSWebSocketHost].is_active()) {
|
||||
overlay::windows::OBS_CONTROL_HOST = options[launcher::Options::OBSWebSocketHost].value_text();
|
||||
}
|
||||
if (options[launcher::Options::OBSWebSocketPort].is_active()) {
|
||||
const auto obs_port = options[launcher::Options::OBSWebSocketPort].value_uint32();
|
||||
if (obs_port > 0 && obs_port <= 65535) {
|
||||
overlay::windows::OBS_CONTROL_PORT = static_cast<uint16_t>(obs_port);
|
||||
}
|
||||
}
|
||||
if (options[launcher::Options::OBSWebSocketPassword].is_active()) {
|
||||
overlay::windows::OBS_CONTROL_PASSWORD = options[launcher::Options::OBSWebSocketPassword].value_text();
|
||||
}
|
||||
if (options[launcher::Options::OBSWebSocketDebug].value_bool()) {
|
||||
overlay::windows::OBS_CONTROL_DEBUG = true;
|
||||
}
|
||||
|
||||
// API debugging
|
||||
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
|
||||
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
||||
@@ -1546,8 +1613,19 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
// print out conflicts
|
||||
size_t conflicts = 0;
|
||||
for (const auto &option : options) {
|
||||
if (option.conflicting && option.get_definition().type != OptionType::Bool) {
|
||||
for (size_t i = 0; i < options.size(); i++) {
|
||||
// InjectHook / EarlyInjectHook accept multiple values, so command line and
|
||||
// spicecfg entries are merged rather than conflicting; don't warn about them
|
||||
if (i == (size_t) launcher::Options::InjectHook ||
|
||||
i == (size_t) launcher::Options::EarlyInjectHook) {
|
||||
continue;
|
||||
}
|
||||
const auto &option = options[i];
|
||||
// ignore Boolean values
|
||||
if (option.get_definition().type == OptionType::Bool) {
|
||||
continue;
|
||||
}
|
||||
if (option.conflicting) {
|
||||
conflicts += 1;
|
||||
const auto& value = option.get_definition().sensitive ? "*****" : option.value;
|
||||
if (launcher::USE_CMD_OVERRIDE) {
|
||||
|
||||
@@ -52,6 +52,7 @@ static const std::vector<std::string> CATEGORY_ORDER_NETWORK = {
|
||||
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
|
||||
"General Overlay",
|
||||
"Game Overlay",
|
||||
"OBS Control",
|
||||
};
|
||||
|
||||
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
|
||||
@@ -433,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",
|
||||
},
|
||||
@@ -504,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 %",
|
||||
@@ -1025,6 +1038,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// SDVXDisableLive2D
|
||||
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||
.name = "sdvxnolive2d",
|
||||
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||
"off: leave Live2D as-is.\n"
|
||||
"always: hide Live2D (also hides menu navigator).\n"
|
||||
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
.elements = {
|
||||
{"off", "Show Live2D"},
|
||||
{"always", "Hide Live2D"},
|
||||
{"ingame", "Hide during songs"},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// spice2x_SDVXSubPos
|
||||
.title = "SDVX Subscreen Overlay Position",
|
||||
@@ -1138,6 +1169,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.game_name = "Pop'n Music",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
{
|
||||
// PopnSubRedraw
|
||||
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
||||
.name = "popnsubredraw",
|
||||
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
||||
"this option forces subscreen to redraw every frame.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Pop'n Music",
|
||||
.category = "Advanced Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
.title = "Force Load HELLO! Pop'n Music Module",
|
||||
.name = "hpm",
|
||||
@@ -1742,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",
|
||||
},
|
||||
@@ -1766,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)",
|
||||
@@ -1777,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",
|
||||
},
|
||||
{
|
||||
@@ -1854,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. "
|
||||
@@ -2638,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",
|
||||
@@ -2645,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",
|
||||
@@ -2663,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"},
|
||||
},
|
||||
},
|
||||
@@ -2678,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",
|
||||
@@ -3190,6 +3303,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "Development",
|
||||
},
|
||||
{
|
||||
// OBSWebSocketEnabled
|
||||
.title = "OBS WebSocket Enable",
|
||||
.name = "obsenable",
|
||||
.desc = "Enables the in-game OBS Control overlay and its connection to the OBS Studio "
|
||||
"obs-websocket (v5) server.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "OBS Control",
|
||||
},
|
||||
{
|
||||
// OBSWebSocketHost
|
||||
.title = "OBS WebSocket Host",
|
||||
.name = "obshost",
|
||||
.desc = "Host name or IP address of the OBS Studio obs-websocket (v5) server used by "
|
||||
"the in-game OBS Control overlay. Defaults to 127.0.0.1 when left empty.",
|
||||
.type = OptionType::Text,
|
||||
.category = "OBS Control",
|
||||
},
|
||||
{
|
||||
// OBSWebSocketPort
|
||||
.title = "OBS WebSocket Port",
|
||||
.name = "obsport",
|
||||
.desc = "Port of the OBS Studio obs-websocket (v5) server. Defaults to 4455 when left empty.",
|
||||
.type = OptionType::Integer,
|
||||
.category = "OBS Control",
|
||||
},
|
||||
{
|
||||
// OBSWebSocketPassword
|
||||
.title = "OBS WebSocket Password",
|
||||
.name = "obspass",
|
||||
.desc = "Password for the OBS Studio obs-websocket (v5) server. Leave empty if "
|
||||
"authentication is disabled in OBS.",
|
||||
.type = OptionType::Text,
|
||||
.category = "OBS Control",
|
||||
.sensitive = true,
|
||||
},
|
||||
{
|
||||
// OBSWebSocketDebug
|
||||
.title = "OBS WebSocket Debug",
|
||||
.name = "obsdebug",
|
||||
.desc = "Writes the OBS WebSocket client's internal connection diagnostics to the log. "
|
||||
"Only enable this when troubleshooting connection problems.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "OBS Control",
|
||||
},
|
||||
};
|
||||
|
||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace launcher {
|
||||
SOFTID,
|
||||
VREnable,
|
||||
DisableOverlay,
|
||||
OverlayKeyboardNavigation,
|
||||
OverlayScaling,
|
||||
NotificationPosition,
|
||||
spice2x_FpsAutoShow,
|
||||
@@ -97,6 +98,7 @@ namespace launcher {
|
||||
SDVXDigitalKnobSocd,
|
||||
spice2x_SDVXAsioDriver,
|
||||
SDVXAsioTwoChannel,
|
||||
SDVXDisableLive2D,
|
||||
spice2x_SDVXSubPos,
|
||||
SDVXSubMonitorOverride,
|
||||
LoadDDRModule,
|
||||
@@ -108,6 +110,7 @@ namespace launcher {
|
||||
PopnNoSub,
|
||||
PopnSubMonitorOverride,
|
||||
PopnNativeTouch,
|
||||
PopnSubRedraw,
|
||||
LoadHelloPopnMusicModule,
|
||||
LoadGitaDoraModule,
|
||||
GitaDoraTwoChannelAudio,
|
||||
@@ -177,6 +180,7 @@ namespace launcher {
|
||||
ForceWinTouch,
|
||||
ForceTouchEmulation,
|
||||
InvertTouchCoordinates,
|
||||
RawInputTouchAspectRatio,
|
||||
DisableTouchCardInsert,
|
||||
spice2x_TouchCardInsert,
|
||||
ICCAReaderPort,
|
||||
@@ -260,6 +264,9 @@ namespace launcher {
|
||||
IIDXWindowedSubscreenAlwaysOnTop,
|
||||
spice2x_JubeatLegacyTouch,
|
||||
JubeatTouchAlgo,
|
||||
JubeatTouchDebug,
|
||||
JubeatTouchDebounce,
|
||||
RBTouchDebug,
|
||||
spice2x_RBTouchScale,
|
||||
RBTouchSize,
|
||||
RBTouchPollRate,
|
||||
@@ -304,7 +311,12 @@ namespace launcher {
|
||||
DisableHighResTimer,
|
||||
EnableICMPHook,
|
||||
AutoElevate,
|
||||
CfgForceSoftwareRender
|
||||
CfgForceSoftwareRender,
|
||||
OBSWebSocketEnabled,
|
||||
OBSWebSocketHost,
|
||||
OBSWebSocketPort,
|
||||
OBSWebSocketPassword,
|
||||
OBSWebSocketDebug
|
||||
};
|
||||
|
||||
enum class OptionsCategory {
|
||||
|
||||
@@ -1327,6 +1327,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
easywsclient (MIT)
|
||||
-------------------------------------------
|
||||
Copyright (c) 2012 Dhruv Matani, Daniel Baird
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Contributions
|
||||
-------------------------------------------
|
||||
cardio - Felix - MIT License
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "extensions.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "external/imgui/imgui_internal.h"
|
||||
#include "overlay/overlay.h"
|
||||
|
||||
|
||||
@@ -160,6 +162,19 @@ namespace ImGui {
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool ColoredButton(const char* label, const ImVec4& base, const ImVec2& size) {
|
||||
const auto brighten = [](const ImVec4& c, float d) {
|
||||
return ImVec4((std::min)(c.x + d, 1.0f), (std::min)(c.y + d, 1.0f),
|
||||
(std::min)(c.z + d, 1.0f), c.w);
|
||||
};
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, base);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(base, 0.12f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(base, 0.22f));
|
||||
const bool clicked = ImGui::Button(label, size);
|
||||
ImGui::PopStyleColor(3);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool ClearButton(const std::string& tooltip) {
|
||||
ImGui::PushID(tooltip.c_str());
|
||||
// same colors as a checkbox
|
||||
@@ -209,17 +224,17 @@ namespace ImGui {
|
||||
return open;
|
||||
}
|
||||
|
||||
void InvisibleTableRowSelectable() {
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, 0);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, 0);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, 0);
|
||||
ImGui::PushTabStop(false); // prevent tab navigation
|
||||
ImGui::Selectable("##row", false,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap);
|
||||
ImGui::PopTabStop();
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
void HighlightTableRowOnHover() {
|
||||
// hit-test the row rect directly so the row layout and height are untouched
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
if (table == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (ImGui::IsWindowHovered() &&
|
||||
ImGui::IsMouseHoveringRect(
|
||||
ImVec2(table->WorkRect.Min.x, table->RowPosY1),
|
||||
ImVec2(table->WorkRect.Max.x, table->RowPosY2),
|
||||
false)) {
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@ namespace ImGui {
|
||||
void TextTruncated(const std::string& p_text, float p_truncated_width);
|
||||
bool DeleteButton(const std::string& tooltip);
|
||||
bool ClearButton(const std::string& tooltip);
|
||||
void InvisibleTableRowSelectable();
|
||||
void HighlightTableRowOnHover();
|
||||
|
||||
// a Button with the given base fill color; the hovered/active shades are
|
||||
// derived by brightening the base. size defaults to auto (fit the label).
|
||||
bool ColoredButton(const char* label, const ImVec4& base,
|
||||
const ImVec2& size = ImVec2(0, 0));
|
||||
|
||||
// Config tab bar with extra label padding and uniform, centered tab widths.
|
||||
// Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar.
|
||||
|
||||
@@ -210,7 +210,15 @@ bool ImGui_ImplSpice_UpdateMouseCursor() {
|
||||
|
||||
// update cursor
|
||||
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
|
||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) {
|
||||
|
||||
// in auto-hide mode imgui owns cursor drawing, so the OS cursor must stay
|
||||
// hidden. this function is also called from the game window's WM_SETCURSOR
|
||||
// handler, which fires on every mouse move; without forcing it hidden here,
|
||||
// the else branch below would set IDC_ARROW on the next mouse move once the
|
||||
// overlay is closed. that arrow is conspicuous on games whose window class
|
||||
// has hCursor==NULL (e.g. DDR), since those normally show no client-area
|
||||
// cursor at all.
|
||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor || g_MouseCursorAutoHide) {
|
||||
|
||||
// hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
||||
::SetCursor(nullptr);
|
||||
@@ -592,8 +600,12 @@ void ImGui_ImplSpice_NewFrame() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// update OS mouse cursor with the cursor requested by imgui
|
||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
||||
// in auto-hide mode imgui owns the cursor, so keep the OS cursor hidden
|
||||
// even when the overlay is closed (io.MouseDrawCursor is false then).
|
||||
ImGuiMouseCursor mouse_cursor =
|
||||
(io.MouseDrawCursor || g_MouseCursorAutoHide)
|
||||
? ImGuiMouseCursor_None
|
||||
: ImGui::GetMouseCursor();
|
||||
if (g_LastMouseCursor != mouse_cursor) {
|
||||
g_LastMouseCursor = mouse_cursor;
|
||||
ImGui_ImplSpice_UpdateMouseCursor();
|
||||
|
||||
@@ -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"
|
||||
@@ -50,6 +51,7 @@
|
||||
#include "windows/sdvx_sub.h"
|
||||
#include "windows/keypad.h"
|
||||
#include "windows/log.h"
|
||||
#include "windows/obs.h"
|
||||
#include "windows/patch_manager.h"
|
||||
#include "windows/exitprompt.cpp"
|
||||
|
||||
@@ -65,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;
|
||||
@@ -310,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;
|
||||
}
|
||||
@@ -416,6 +422,12 @@ void overlay::SpiceOverlay::init() {
|
||||
}
|
||||
this->window_add(new overlay::windows::PatchManager(this));
|
||||
|
||||
// OBS control spawns a background WebSocket worker; skip it in the standalone
|
||||
// configurator, where there is no running game to stream/record
|
||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||
this->window_add(window_obs = new overlay::windows::OBSControl(this));
|
||||
}
|
||||
|
||||
{
|
||||
window_keypad1 = new overlay::windows::Keypad(this, 0);
|
||||
this->window_add(window_keypad1);
|
||||
@@ -544,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;
|
||||
}
|
||||
|
||||
@@ -571,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) {
|
||||
@@ -588,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;
|
||||
@@ -77,6 +78,7 @@ namespace overlay {
|
||||
Window *window_camera = nullptr;
|
||||
Window *window_sub = nullptr;
|
||||
Window *window_log = nullptr;
|
||||
Window *window_obs = nullptr;
|
||||
|
||||
// not part of `windows`: drawn/updated on the persistent layer (like
|
||||
// notifications), independent of the overlay's active state and input gates.
|
||||
|
||||
@@ -928,18 +928,14 @@ namespace overlay::windows {
|
||||
|
||||
// primary
|
||||
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
|
||||
ImGui::PushID(&primary_button);
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::PopID();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
|
||||
// alternatives
|
||||
int alt_index = 1; // 0 is primary
|
||||
for (auto &alt : primary_button.getAlternatives()) {
|
||||
if (alt.isValid()) {
|
||||
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
|
||||
ImGui::PushID(&alt);
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::PopID();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
}
|
||||
alt_index++;
|
||||
}
|
||||
@@ -2519,8 +2515,7 @@ namespace overlay::windows {
|
||||
|
||||
edit_analog_popup(analog, title);
|
||||
|
||||
// row hover detection (invisible selectable that spans entire row)
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
|
||||
// clean up
|
||||
ImGui::PopID();
|
||||
@@ -3174,16 +3169,12 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
build_light(light, &light, i, 0);
|
||||
ImGui::PushID(&light);
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::PopID();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
int alt_index = 1;
|
||||
for (auto &alt : light.getAlternatives()) {
|
||||
if (alt.isValid()) {
|
||||
build_light(light, &alt, i, alt_index);
|
||||
ImGui::PushID(&alt);
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::PopID();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
}
|
||||
alt_index++;
|
||||
}
|
||||
@@ -4209,8 +4200,10 @@ namespace overlay::windows {
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(
|
||||
"Specify hardcoded card numbers here. This will always take priority when Insert Card is pressed.");
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
"Specify hardcoded card numbers here.\n\n"
|
||||
"Overrides will always take priority when Insert Card is pressed.");
|
||||
ImGui::Spacing();
|
||||
|
||||
// read in values from options
|
||||
@@ -4387,7 +4380,8 @@ namespace overlay::windows {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
"Use text files on disk; its content will be read when Insert Card is pressed.");
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -5293,8 +5287,7 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// row hover detection (invisible selectable that spans entire row)
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
|
||||
// next item
|
||||
ImGui::PopID();
|
||||
@@ -5787,23 +5780,28 @@ namespace overlay::windows {
|
||||
|
||||
// name
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextTruncated(
|
||||
t.name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
|
||||
|
||||
// type
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User");
|
||||
|
||||
// buttons
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%d", (int)t.buttons.size());
|
||||
|
||||
// analogs
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%d", (int)t.analogs.size());
|
||||
|
||||
// lights
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%d", (int)t.lights.size());
|
||||
|
||||
// actions
|
||||
@@ -5831,7 +5829,7 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
|
||||
@@ -119,8 +119,9 @@ namespace overlay::windows {
|
||||
ImGui::TextDisabled("Graphics");
|
||||
build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE);
|
||||
|
||||
build_button(this->overlay->window_fps.get(), "FPS", size_half, NextItem::SAME_LINE);
|
||||
build_button(this->overlay->window_resize, "Resize", size_half, NextItem::NEW_LINE);
|
||||
build_button(this->overlay->window_fps.get(), "FPS", size_third, NextItem::SAME_LINE);
|
||||
build_button(this->overlay->window_obs, "OBS", size_third, NextItem::SAME_LINE);
|
||||
build_button(this->overlay->window_resize, "Resize", size_third, NextItem::NEW_LINE);
|
||||
|
||||
ImGui::TextDisabled("I/O");
|
||||
build_button(this->overlay->window_cards, "Card Manager", size, NextItem::NEW_LINE);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <algorithm>
|
||||
#include "external/fmt/include/fmt/chrono.h"
|
||||
#include "fps.h"
|
||||
#include "obs.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
@@ -14,6 +15,7 @@ namespace overlay::windows {
|
||||
this->title = "Stats";
|
||||
this->flags = ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_AlwaysAutoResize
|
||||
| ImGuiWindowFlags_NoCollapse
|
||||
| ImGuiWindowFlags_NoFocusOnAppearing
|
||||
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
||||
@@ -29,25 +31,7 @@ namespace overlay::windows {
|
||||
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
|
||||
}
|
||||
|
||||
void FPS::calculate_initial_window() {
|
||||
// size the window explicitly (no AlwaysAutoResize) so the corner anchoring
|
||||
// below is exact; the footprint mirrors the fixed-fit table in build_content()
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const int rows = 3;
|
||||
|
||||
// widest label and widest value drive the two fixed-fit columns
|
||||
const float label_w = (std::max)(
|
||||
ImGui::CalcTextSize("Time").x,
|
||||
ImGui::CalcTextSize("Game").x);
|
||||
const float value_w = ImGui::CalcTextSize("00:00:00").x;
|
||||
|
||||
const float win_w = label_w + value_w
|
||||
+ FPS_CELL_PADDING.x * 2
|
||||
+ FPS_WINDOW_PADDING.x * 2;
|
||||
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * rows
|
||||
+ FPS_WINDOW_PADDING.y * 2;
|
||||
this->init_size = ImVec2(win_w, win_h);
|
||||
|
||||
ImVec2 FPS::anchored_pos(const ImVec2 &size) const {
|
||||
// bottom-anchored windows use a larger edge margin (matching notification
|
||||
// toasts) since they overlap the same on-screen UI; other edges hug closer
|
||||
const float edge_margin = overlay::apply_scaling(4);
|
||||
@@ -61,9 +45,27 @@ namespace overlay::windows {
|
||||
overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft ||
|
||||
overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight;
|
||||
|
||||
const float pos_x = right ? display.x - win_w - edge_margin : edge_margin;
|
||||
const float pos_y = bottom ? display.y - win_h - bottom_margin : edge_margin;
|
||||
this->init_pos = ImVec2(pos_x, pos_y);
|
||||
const float pos_x = right ? display.x - size.x - edge_margin : edge_margin;
|
||||
const float pos_y = bottom ? display.y - size.y - bottom_margin : edge_margin;
|
||||
return ImVec2(pos_x, pos_y);
|
||||
}
|
||||
|
||||
void FPS::calculate_initial_window() {
|
||||
// first-frame size estimate for the base 3 rows; AlwaysAutoResize handles
|
||||
// the exact size (incl. any OBS rows) and build_content re-anchors each frame
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float label_w = (std::max)(
|
||||
ImGui::CalcTextSize("Time").x,
|
||||
ImGui::CalcTextSize("Game").x);
|
||||
const float value_w = ImGui::CalcTextSize("00:00:00").x;
|
||||
|
||||
const float win_w = label_w + value_w
|
||||
+ FPS_CELL_PADDING.x * 2
|
||||
+ FPS_WINDOW_PADDING.x * 2;
|
||||
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * 3
|
||||
+ FPS_WINDOW_PADDING.y * 2;
|
||||
this->init_size = ImVec2(win_w, win_h);
|
||||
this->init_pos = this->anchored_pos(this->init_size);
|
||||
}
|
||||
|
||||
void FPS::build_content() {
|
||||
@@ -79,6 +81,21 @@ namespace overlay::windows {
|
||||
|
||||
const auto uptime = now_s - this->start_time;
|
||||
|
||||
// OBS status (only adds rows while streaming live or recording/paused)
|
||||
OBSStatus obs_status;
|
||||
bool show_stream = false;
|
||||
bool show_record = false;
|
||||
if (auto *obs = static_cast<OBSControl *>(this->overlay->window_obs)) {
|
||||
obs_status = obs->get_status();
|
||||
show_stream = obs_status.streaming;
|
||||
show_record = obs_status.recording;
|
||||
}
|
||||
|
||||
// AlwaysAutoResize sizes the window to its content, so adding/removing OBS
|
||||
// rows never clips; just re-anchor it to the configured corner each frame
|
||||
// using the actual (auto-sized) dimensions
|
||||
ImGui::SetWindowPos(this->anchored_pos(ImGui::GetWindowSize()), ImGuiCond_Always);
|
||||
|
||||
// right-align a label within the current cell so the label column reads
|
||||
// flush against the value column instead of looking ragged. the label is
|
||||
// only slightly dimmer than normal text (not the much darker "disabled" tone)
|
||||
@@ -117,6 +134,31 @@ namespace overlay::windows {
|
||||
fmt::format("{:%H:%M:%S}",
|
||||
std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
|
||||
|
||||
// OBS rows - only present while live or recording
|
||||
const ImVec4 col_red(0.90f, 0.30f, 0.30f, 1.0f);
|
||||
const ImVec4 col_yellow(0.95f, 0.80f, 0.30f, 1.0f);
|
||||
if (show_stream) {
|
||||
const int64_t ms = OBSControl::live_duration_ms(
|
||||
obs_status.stream_duration_ms, obs_status.stream_duration_base_tick, true);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
label("Live");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextColored(col_red, "%s",
|
||||
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
|
||||
}
|
||||
if (show_record) {
|
||||
const int64_t ms = OBSControl::live_duration_ms(
|
||||
obs_status.record_duration_ms, obs_status.record_duration_base_tick,
|
||||
!obs_status.record_paused);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
label("Rec");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextColored(obs_status.record_paused ? col_yellow : col_red, "%s",
|
||||
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace overlay::windows {
|
||||
private:
|
||||
std::chrono::system_clock::time_point start_time;
|
||||
|
||||
// anchored top-left position for a window of the given size, per FPS_LOCATION
|
||||
ImVec2 anchored_pos(const ImVec2 &size) const;
|
||||
|
||||
public:
|
||||
|
||||
FPS(SpiceOverlay *overlay);
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
#include "obs.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
||||
#include "external/imgui/imgui.h"
|
||||
|
||||
#include "games/io.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/imgui/extensions.h"
|
||||
|
||||
using namespace std::chrono;
|
||||
|
||||
// OBS WebSocket protocol/worker thread lives in obs_websocket.cpp; this file
|
||||
// owns the ImGui control window and the connection lifecycle.
|
||||
|
||||
namespace {
|
||||
|
||||
// status text colors
|
||||
const ImVec4 COL_GREEN(0.40f, 0.85f, 0.40f, 1.0f);
|
||||
const ImVec4 COL_RED(0.90f, 0.30f, 0.30f, 1.0f);
|
||||
const ImVec4 COL_YELLOW(0.95f, 0.80f, 0.30f, 1.0f);
|
||||
const ImVec4 COL_GREY(0.60f, 0.60f, 0.60f, 1.0f);
|
||||
|
||||
// muted action-button fills (start = green, stop = red, pause = yellow); the
|
||||
// hovered/active shades are derived by brightening the base
|
||||
const ImVec4 COL_BTN_GREEN(0.20f, 0.45f, 0.24f, 1.0f);
|
||||
const ImVec4 COL_BTN_RED(0.52f, 0.20f, 0.20f, 1.0f);
|
||||
const ImVec4 COL_BTN_YELLOW(0.52f, 0.42f, 0.16f, 1.0f);
|
||||
|
||||
// an in-flight request lingers for at most this long before the button frees
|
||||
// itself, so a dropped state event can never wedge a control permanently
|
||||
const int64_t PENDING_TIMEOUT_MS = 5000;
|
||||
|
||||
int64_t now_tick_ms() {
|
||||
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
std::string format_duration(int64_t ms) {
|
||||
if (ms < 0) {
|
||||
ms = 0;
|
||||
}
|
||||
const int64_t total_seconds = ms / 1000;
|
||||
const int64_t hours = total_seconds / 3600;
|
||||
const int64_t minutes = (total_seconds % 3600) / 60;
|
||||
const int64_t seconds = total_seconds % 60;
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%02lld:%02lld:%02lld",
|
||||
static_cast<long long>(hours),
|
||||
static_cast<long long>(minutes),
|
||||
static_cast<long long>(seconds));
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
OBSControl::OBSControl(SpiceOverlay *overlay) : Window(overlay) {
|
||||
this->title = "OBS Control";
|
||||
this->flags |= ImGuiWindowFlags_AlwaysAutoResize;
|
||||
this->init_pos = overlay::apply_scaling_to_vector(120, 120);
|
||||
this->toggle_button = games::OverlayButtons::ToggleOBSControl;
|
||||
|
||||
this->worker_running.store(true);
|
||||
this->worker_thread = std::thread(&OBSControl::worker_main, this);
|
||||
}
|
||||
|
||||
OBSControl::~OBSControl() {
|
||||
// signal stop and wake any in-progress interruptible_sleep at once; the
|
||||
// lock around the store pairs with the wait predicate to avoid a lost wakeup
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->worker_mutex);
|
||||
this->worker_running.store(false);
|
||||
}
|
||||
this->worker_cv.notify_all();
|
||||
if (this->worker_thread.joinable()) {
|
||||
// note: if the worker is mid-connect, WebSocket::from_url performs a
|
||||
// blocking getaddrinfo/connect that does not observe worker_running,
|
||||
// so this join can stall for the OS connect timeout. the default
|
||||
// 127.0.0.1 host fails fast (connection refused); only a misconfigured
|
||||
// unreachable remote OBS_CONTROL_HOST would delay shutdown here.
|
||||
this->worker_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
OBSStatus OBSControl::get_status() {
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
return this->status;
|
||||
}
|
||||
|
||||
int64_t OBSControl::live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking) {
|
||||
if (!ticking) {
|
||||
return (std::max<int64_t>)(base_ms, 0);
|
||||
}
|
||||
const int64_t now =
|
||||
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||
// clamp so a stale base tick / clock hiccup can never yield a negative
|
||||
// duration; callers (FPS rows, build_content) format this directly
|
||||
return (std::max<int64_t>)(base_ms + (now - base_tick), 0);
|
||||
}
|
||||
|
||||
void OBSControl::build_content() {
|
||||
|
||||
const OBSStatus s = this->get_status();
|
||||
|
||||
// label + colored value on a single line
|
||||
const auto status_line = [](const char *label, const ImVec4 &col, const char *value) {
|
||||
ImGui::Text("%s", label);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(col, "%s", value);
|
||||
};
|
||||
|
||||
if (!s.connected) {
|
||||
if (s.disabled) {
|
||||
ImGui::TextColored(COL_GREY, "%s", "OBS Control is disabled");
|
||||
return;
|
||||
}
|
||||
if (s.identifying) {
|
||||
status_line("OBS WebSocket:", COL_YELLOW, "Connecting...");
|
||||
} else {
|
||||
status_line("OBS WebSocket:", COL_GREY, "Not connected");
|
||||
}
|
||||
const std::string url =
|
||||
"ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
|
||||
status_line("Address:", COL_GREY, url.c_str());
|
||||
if (!s.connection_error.empty()) {
|
||||
ImGui::TextColored(COL_RED, "%s", s.connection_error.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
status_line("OBS WebSocket:", COL_GREEN, "Connected");
|
||||
|
||||
// one fixed content width drives the whole panel so it never resizes as
|
||||
// the scene name or button labels change; every row is sized to fit it
|
||||
const float spacing = ImGui::GetStyle().ItemSpacing.x;
|
||||
const float row_w = overlay::apply_scaling(240);
|
||||
|
||||
if (s.current_scene.empty()) {
|
||||
status_line("Scene:", COL_GREY, "(unknown)");
|
||||
} else {
|
||||
ImGui::Text("Scene:");
|
||||
ImGui::SameLine();
|
||||
// truncate to the remaining row width so "Scene:" + value together
|
||||
// never overflow and push the window wider
|
||||
const float label_w = ImGui::CalcTextSize("Scene:").x;
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, COL_GREY);
|
||||
ImGui::TextTruncated(s.current_scene, row_w - label_w - spacing);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
const int64_t now = now_tick_ms();
|
||||
// every button shares one fixed size; two side-by-side fill the row width,
|
||||
// single buttons keep that same size rather than stretching to fill
|
||||
const ImVec2 btn((row_w - spacing) * 0.5f, 0);
|
||||
|
||||
// has OBS reached the state a pending action was waiting for?
|
||||
const auto reached = [&](OBSAction a) {
|
||||
switch (a) {
|
||||
case OBSAction::StreamStart: return s.streaming;
|
||||
case OBSAction::StreamStop: return !s.streaming;
|
||||
case OBSAction::RecordStart: return s.recording;
|
||||
case OBSAction::RecordStop: return !s.recording;
|
||||
case OBSAction::RecordPause: return s.record_paused;
|
||||
case OBSAction::RecordResume: return !s.record_paused;
|
||||
default: return true;
|
||||
}
|
||||
};
|
||||
|
||||
// drop a pending action once OBS confirms the new state, or once the
|
||||
// safety deadline lapses (so a dropped event can't wedge the button)
|
||||
const auto settle = [&](OBSAction &slot, int64_t deadline) {
|
||||
if (slot != OBSAction::None && (reached(slot) || now >= deadline)) {
|
||||
slot = OBSAction::None;
|
||||
}
|
||||
};
|
||||
settle(this->stream_pending, this->stream_pending_deadline);
|
||||
settle(this->record_pending, this->record_pending_deadline);
|
||||
|
||||
// a colored button that fires a request and marks the output busy on click
|
||||
const auto action_button =
|
||||
[&](const char *label,
|
||||
const ImVec4 &color,
|
||||
const char *request,
|
||||
OBSAction &slot,
|
||||
int64_t &deadline,
|
||||
OBSAction action) {
|
||||
|
||||
if (ImGui::ColoredButton(label, color, btn)) {
|
||||
enqueue_request(request);
|
||||
slot = action;
|
||||
deadline = now + PENDING_TIMEOUT_MS;
|
||||
}
|
||||
};
|
||||
|
||||
// streaming
|
||||
{
|
||||
const bool pending = this->stream_pending != OBSAction::None;
|
||||
|
||||
if (s.streaming) {
|
||||
const int64_t ms = live_duration_ms(
|
||||
s.stream_duration_ms, s.stream_duration_base_tick, true);
|
||||
status_line("Streaming:", COL_RED, ("LIVE " + format_duration(ms)).c_str());
|
||||
} else {
|
||||
status_line("Streaming:", COL_GREY, pending ? "Starting..." : "Idle");
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(pending);
|
||||
if (s.streaming) {
|
||||
action_button(
|
||||
pending ? "Stopping...##stream" : "Stop Streaming##stream",
|
||||
COL_BTN_RED,
|
||||
"StopStream",
|
||||
this->stream_pending,
|
||||
this->stream_pending_deadline,
|
||||
OBSAction::StreamStop);
|
||||
} else {
|
||||
action_button(
|
||||
pending ? "Starting...##stream" : "Start Streaming##stream",
|
||||
COL_BTN_GREEN,
|
||||
"StartStream",
|
||||
this->stream_pending,
|
||||
this->stream_pending_deadline,
|
||||
OBSAction::StreamStart);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// recording
|
||||
{
|
||||
const bool pending = this->record_pending != OBSAction::None;
|
||||
|
||||
if (!s.recording) {
|
||||
status_line("Recording:", COL_GREY, pending ? "Starting..." : "Idle");
|
||||
ImGui::BeginDisabled(pending);
|
||||
action_button(
|
||||
pending ? "Starting...##record" : "Start Recording##record",
|
||||
COL_BTN_GREEN,
|
||||
"StartRecord",
|
||||
this->record_pending,
|
||||
this->record_pending_deadline,
|
||||
OBSAction::RecordStart);
|
||||
ImGui::EndDisabled();
|
||||
return;
|
||||
}
|
||||
|
||||
const int64_t ms = live_duration_ms(
|
||||
s.record_duration_ms, s.record_duration_base_tick, !s.record_paused);
|
||||
if (s.record_paused) {
|
||||
status_line("Recording:", COL_YELLOW, ("PAUSED " + format_duration(ms)).c_str());
|
||||
} else {
|
||||
status_line("Recording:", COL_RED, ("REC " + format_duration(ms)).c_str());
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(pending);
|
||||
action_button(
|
||||
this->record_pending == OBSAction::RecordStop ? "Stopping...##record" : "Stop Recording##record",
|
||||
COL_BTN_RED,
|
||||
"StopRecord",
|
||||
this->record_pending,
|
||||
this->record_pending_deadline, OBSAction::RecordStop);
|
||||
|
||||
ImGui::SameLine();
|
||||
if (s.record_paused) {
|
||||
action_button(
|
||||
this->record_pending == OBSAction::RecordResume ? "Resuming...##record_toggle" : "Resume##record_toggle",
|
||||
COL_BTN_GREEN,
|
||||
"ResumeRecord",
|
||||
this->record_pending,
|
||||
this->record_pending_deadline,
|
||||
OBSAction::RecordResume);
|
||||
|
||||
} else {
|
||||
action_button(
|
||||
this->record_pending == OBSAction::RecordPause ? "Pausing...##record_toggle" : "Pause##record_toggle",
|
||||
COL_BTN_YELLOW,
|
||||
"PauseRecord",
|
||||
this->record_pending,
|
||||
this->record_pending_deadline,
|
||||
OBSAction::RecordPause);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "external/rapidjson/fwd.h"
|
||||
#include "overlay/window.h"
|
||||
|
||||
namespace easywsclient {
|
||||
class WebSocket;
|
||||
}
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// OBS WebSocket connection settings, resolved once at launch from the merged
|
||||
// launcher options (command line + saved config) following the same pattern
|
||||
// as the other global launch settings in launcher.cpp
|
||||
extern bool OBS_CONTROL_ENABLED;
|
||||
extern std::string OBS_CONTROL_HOST;
|
||||
extern uint16_t OBS_CONTROL_PORT;
|
||||
extern std::string OBS_CONTROL_PASSWORD;
|
||||
|
||||
// when true, easywsclient's internal diagnostics are routed to the logger
|
||||
extern bool OBS_CONTROL_DEBUG;
|
||||
|
||||
// status snapshot shared between the OBS worker thread and the render thread
|
||||
struct OBSStatus {
|
||||
bool disabled = true;
|
||||
bool connected = false;
|
||||
bool identifying = false;
|
||||
std::string connection_error;
|
||||
|
||||
// name of the active program scene (read-only, from obs-websocket)
|
||||
std::string current_scene;
|
||||
|
||||
bool streaming = false;
|
||||
bool recording = false;
|
||||
bool record_paused = false;
|
||||
|
||||
// duration base values (milliseconds) and the local timestamp (ms since
|
||||
// steady epoch) at which they were last refreshed, so the UI can tick a
|
||||
// smooth timer between polls
|
||||
int64_t stream_duration_ms = 0;
|
||||
int64_t record_duration_ms = 0;
|
||||
int64_t stream_duration_base_tick = 0;
|
||||
int64_t record_duration_base_tick = 0;
|
||||
};
|
||||
|
||||
// in-flight user action used purely for UI feedback: when the user clicks a
|
||||
// control we remember what we asked for so the button can show a transitional
|
||||
// label and stay disabled until the observed OBS state matches the request
|
||||
// (or a short deadline lapses). owned solely by the render thread.
|
||||
enum class OBSAction {
|
||||
None,
|
||||
StreamStart, StreamStop,
|
||||
RecordStart, RecordStop,
|
||||
RecordPause, RecordResume,
|
||||
};
|
||||
|
||||
class OBSControl : public Window {
|
||||
public:
|
||||
OBSControl(SpiceOverlay *overlay);
|
||||
~OBSControl() override;
|
||||
|
||||
void build_content() override;
|
||||
|
||||
// thread-safe snapshot of the current status for external widgets (e.g. FPS)
|
||||
OBSStatus get_status();
|
||||
|
||||
// live (ticked) duration in ms from a base value/tick captured at last poll
|
||||
static int64_t live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking);
|
||||
|
||||
private:
|
||||
// worker thread entry + helpers (implementation owns the WebSocket)
|
||||
void worker_main();
|
||||
|
||||
// run one connected session loop until the socket closes or we stop;
|
||||
// returns true if the obs-websocket handshake reached "Identified", false
|
||||
// if the socket closed first (e.g. OBS rejected our auth)
|
||||
bool run_session(easywsclient::WebSocket *ws, const std::string &password,
|
||||
uint64_t &request_id);
|
||||
|
||||
// handle a single inbound obs-websocket message (parses + dispatches)
|
||||
void handle_message(easywsclient::WebSocket *ws, const std::string &message,
|
||||
const std::string &password, uint64_t &request_id,
|
||||
bool &identified);
|
||||
|
||||
// per-opcode handlers dispatched from handle_message
|
||||
using request_fn = std::function<void(const char *request_type)>;
|
||||
void handle_identified(bool &identified, const request_fn &request);
|
||||
void handle_event(const rapidjson::Value &d, const request_fn &request);
|
||||
void handle_response(const rapidjson::Value &d);
|
||||
|
||||
void enqueue_request(const std::string &request_type);
|
||||
|
||||
// sleep up to total_ms, waking early if the worker is asked to stop
|
||||
void interruptible_sleep(int total_ms);
|
||||
|
||||
// worker thread
|
||||
std::thread worker_thread;
|
||||
std::atomic<bool> worker_running { false };
|
||||
|
||||
// wakes interruptible_sleep immediately when worker_running is cleared,
|
||||
// so shutdown (and the reconnect backoff) never waits out a fixed delay
|
||||
std::mutex worker_mutex;
|
||||
std::condition_variable worker_cv;
|
||||
|
||||
// shared status (guarded by status_mutex)
|
||||
std::mutex status_mutex;
|
||||
OBSStatus status;
|
||||
|
||||
// outgoing user commands (guarded by command_mutex)
|
||||
std::mutex command_mutex;
|
||||
std::deque<std::string> command_queue;
|
||||
|
||||
// transient action feedback, touched only by the render thread (no sync):
|
||||
// remembers the last start/stop/pause request per output so the button can
|
||||
// show a "Starting.../Stopping..." label and stay disabled until OBS reports
|
||||
// the matching state, with *_deadline as a fallback if the update is missed
|
||||
OBSAction stream_pending = OBSAction::None;
|
||||
OBSAction record_pending = OBSAction::None;
|
||||
int64_t stream_pending_deadline = 0;
|
||||
int64_t record_pending_deadline = 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
#include <winsock2.h>
|
||||
|
||||
#include "obs.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "external/easywsclient/easywsclient.hpp"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "external/rapidjson/stringbuffer.h"
|
||||
#include "external/rapidjson/writer.h"
|
||||
#include "external/hash-library/sha256.h"
|
||||
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
// defined in easywsclient.cpp; gates its internal diagnostic output
|
||||
extern bool EASYWSCLIENT_LOGGING_ENABLED;
|
||||
|
||||
using easywsclient::WebSocket;
|
||||
using namespace std::chrono;
|
||||
|
||||
// obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket):
|
||||
// server -> op 0 Hello (may include an auth challenge)
|
||||
// client -> op 1 Identify (answers the challenge, picks rpcVersion)
|
||||
// server -> op 2 Identified (handshake done; requests may now be sent)
|
||||
// server -> op 5 Event (state changes: stream/record/scene/...)
|
||||
// client -> op 6 Request (e.g. GetStreamStatus, StartRecord)
|
||||
// server -> op 7 RequestResponse (reply to a Request, carries responseData)
|
||||
// Every message is { "op": <int>, "d": { ... } }. Event fields are nested under
|
||||
// d["eventData"] and request replies under d["responseData"], not in d directly.
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t now_ms() {
|
||||
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
// raw SHA256 digest -> base64 (obs-websocket v5 auth primitive)
|
||||
std::string sha256_base64(const std::string &input) {
|
||||
SHA256 hasher;
|
||||
hasher.add(input.data(), input.size());
|
||||
unsigned char digest[SHA256::HashBytes];
|
||||
hasher.getHash(digest);
|
||||
return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes);
|
||||
}
|
||||
|
||||
// auth = base64(sha256(base64(sha256(password + salt)) + challenge))
|
||||
std::string compute_auth(const std::string &password, const std::string &salt,
|
||||
const std::string &challenge) {
|
||||
const std::string secret = sha256_base64(password + salt);
|
||||
return sha256_base64(secret + challenge);
|
||||
}
|
||||
|
||||
std::string build_identify(int rpc_version, const std::string &authentication) {
|
||||
rapidjson::StringBuffer sb;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
|
||||
w.StartObject();
|
||||
w.Key("op"); w.Int(1);
|
||||
w.Key("d");
|
||||
w.StartObject();
|
||||
w.Key("rpcVersion"); w.Int(rpc_version);
|
||||
if (!authentication.empty()) {
|
||||
w.Key("authentication"); w.String(authentication.c_str());
|
||||
}
|
||||
w.EndObject();
|
||||
w.EndObject();
|
||||
return sb.GetString();
|
||||
}
|
||||
|
||||
std::string build_request(const std::string &request_type, uint64_t request_id) {
|
||||
rapidjson::StringBuffer sb;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
|
||||
w.StartObject();
|
||||
w.Key("op"); w.Int(6);
|
||||
w.Key("d");
|
||||
w.StartObject();
|
||||
w.Key("requestType"); w.String(request_type.c_str());
|
||||
w.Key("requestId"); w.String(std::to_string(request_id).c_str());
|
||||
w.EndObject();
|
||||
w.EndObject();
|
||||
return sb.GetString();
|
||||
}
|
||||
|
||||
// read a numeric field as int64 ms (obs sends durations as integers/doubles)
|
||||
int64_t json_number(const rapidjson::Value &obj, const char *key) {
|
||||
if (obj.HasMember(key) && obj[key].IsNumber()) {
|
||||
return static_cast<int64_t>(obj[key].GetDouble());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool json_bool(const rapidjson::Value &obj, const char *key) {
|
||||
return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool();
|
||||
}
|
||||
|
||||
std::string json_string(const rapidjson::Value &obj, const char *key) {
|
||||
if (obj.HasMember(key) && obj[key].IsString()) {
|
||||
return obj[key].GetString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// build the Identify (op 1) reply to a Hello (op 0), answering the auth
|
||||
// challenge if the server requires one
|
||||
std::string build_hello_response(const rapidjson::Value &d, const std::string &password) {
|
||||
int rpc_version = 1;
|
||||
if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) {
|
||||
rpc_version = d["rpcVersion"].GetInt();
|
||||
}
|
||||
std::string auth;
|
||||
if (d.HasMember("authentication") && d["authentication"].IsObject()) {
|
||||
const rapidjson::Value &a = d["authentication"];
|
||||
const std::string challenge = json_string(a, "challenge");
|
||||
const std::string salt = json_string(a, "salt");
|
||||
if (!challenge.empty()) {
|
||||
auth = compute_auth(password, salt, challenge);
|
||||
}
|
||||
}
|
||||
return build_identify(rpc_version, auth);
|
||||
}
|
||||
|
||||
// map an obs-websocket outputState to a user notification. `label` is the
|
||||
// output kind ("Streaming" or "Recording"). transitional states are ignored.
|
||||
void notify_output_state(const char *label, const std::string &state) {
|
||||
using overlay::notifications::Severity;
|
||||
|
||||
struct StateToast {
|
||||
const char *state;
|
||||
Severity severity;
|
||||
const char *verb;
|
||||
};
|
||||
static const StateToast TOASTS[] = {
|
||||
{ "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" },
|
||||
{ "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" },
|
||||
{ "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" },
|
||||
{ "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" },
|
||||
};
|
||||
|
||||
for (const auto &toast : TOASTS) {
|
||||
if (state == toast.state) {
|
||||
overlay::notifications::add(toast.severity,
|
||||
"OBS: " + std::string(label) + " " + toast.verb);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// connection settings resolved at launch (see launcher.cpp)
|
||||
bool OBS_CONTROL_ENABLED = false;
|
||||
std::string OBS_CONTROL_HOST = "127.0.0.1";
|
||||
uint16_t OBS_CONTROL_PORT = 4455;
|
||||
std::string OBS_CONTROL_PASSWORD;
|
||||
bool OBS_CONTROL_DEBUG = false;
|
||||
|
||||
void OBSControl::enqueue_request(const std::string &request_type) {
|
||||
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||
this->command_queue.push_back(request_type);
|
||||
}
|
||||
|
||||
void OBSControl::interruptible_sleep(int total_ms) {
|
||||
std::unique_lock<std::mutex> lock(this->worker_mutex);
|
||||
this->worker_cv.wait_for(lock, milliseconds(total_ms),
|
||||
[this] { return !this->worker_running.load(); });
|
||||
}
|
||||
|
||||
void OBSControl::handle_message(WebSocket *ws, const std::string &message,
|
||||
const std::string &password, uint64_t &request_id, bool &identified) {
|
||||
|
||||
rapidjson::Document doc;
|
||||
if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) {
|
||||
return;
|
||||
}
|
||||
if (!doc.HasMember("op") || !doc["op"].IsInt()
|
||||
|| !doc.HasMember("d") || !doc["d"].IsObject()) {
|
||||
return;
|
||||
}
|
||||
const int op = doc["op"].GetInt();
|
||||
const rapidjson::Value &d = doc["d"];
|
||||
|
||||
// send an op 6 Request; each needs a unique id (we never match replies
|
||||
// back, so a simple incrementing counter is enough)
|
||||
const request_fn request = [&](const char *request_type) {
|
||||
ws->send(build_request(request_type, ++request_id));
|
||||
};
|
||||
|
||||
switch (op) {
|
||||
case 0: // Hello
|
||||
// server greeted us: reply with Identify, solving the auth
|
||||
// challenge inline if the server set a password
|
||||
ws->send(build_hello_response(d, password));
|
||||
break;
|
||||
case 2: // Identified
|
||||
this->handle_identified(identified, request);
|
||||
break;
|
||||
case 5: // Event
|
||||
this->handle_event(d, request);
|
||||
break;
|
||||
case 7: // RequestResponse
|
||||
this->handle_response(d);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OBSControl::handle_identified(bool &identified, const request_fn &request) {
|
||||
// handshake complete: the connection is now usable for requests
|
||||
identified = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.connected = true;
|
||||
this->status.identifying = false;
|
||||
this->status.connection_error.clear();
|
||||
}
|
||||
log_info("obs", "connected and identified");
|
||||
|
||||
// pull the current scene/stream/record state so the UI starts accurate
|
||||
request("GetCurrentProgramScene");
|
||||
request("GetStreamStatus");
|
||||
request("GetRecordStatus");
|
||||
}
|
||||
|
||||
void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) {
|
||||
const std::string type = json_string(d, "eventType");
|
||||
const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject();
|
||||
|
||||
if (type == "StreamStateChanged") {
|
||||
if (has_data) {
|
||||
notify_output_state("Streaming", json_string(d["eventData"], "outputState"));
|
||||
}
|
||||
request("GetStreamStatus");
|
||||
} else if (type == "RecordStateChanged") {
|
||||
if (has_data) {
|
||||
notify_output_state("Recording", json_string(d["eventData"], "outputState"));
|
||||
}
|
||||
request("GetRecordStatus");
|
||||
} else if (type == "CurrentProgramSceneChanged" && has_data) {
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.current_scene = json_string(d["eventData"], "sceneName");
|
||||
}
|
||||
}
|
||||
|
||||
void OBSControl::handle_response(const rapidjson::Value &d) {
|
||||
const std::string type = json_string(d, "requestType");
|
||||
if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) {
|
||||
return;
|
||||
}
|
||||
const rapidjson::Value &rd = d["responseData"];
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
if (type == "GetCurrentProgramScene") {
|
||||
// newer obs returns sceneName; older builds used the now-deprecated
|
||||
// currentProgramSceneName, so prefer it then fall back
|
||||
std::string scene = json_string(rd, "currentProgramSceneName");
|
||||
if (scene.empty()) {
|
||||
scene = json_string(rd, "sceneName");
|
||||
}
|
||||
this->status.current_scene = scene;
|
||||
} else if (type == "GetStreamStatus") {
|
||||
this->status.streaming = json_bool(rd, "outputActive");
|
||||
this->status.stream_duration_ms = json_number(rd, "outputDuration");
|
||||
this->status.stream_duration_base_tick = now_ms();
|
||||
} else if (type == "GetRecordStatus") {
|
||||
this->status.recording = json_bool(rd, "outputActive");
|
||||
this->status.record_paused = json_bool(rd, "outputPaused");
|
||||
this->status.record_duration_ms = json_number(rd, "outputDuration");
|
||||
this->status.record_duration_base_tick = now_ms();
|
||||
}
|
||||
}
|
||||
|
||||
bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) {
|
||||
|
||||
// one iteration of a live connection: pump socket I/O, dispatch any
|
||||
// inbound messages, flush queued user commands, then refresh status
|
||||
bool identified = false;
|
||||
// handle_identified() issues the first GetStreamStatus/GetRecordStatus on
|
||||
// identify, so the periodic poll below just maintains the ~1s cadence
|
||||
auto last_status_poll = steady_clock::now();
|
||||
|
||||
// send a request with the next sequential id
|
||||
const auto request = [&](const char *request_type) {
|
||||
ws->send(build_request(request_type, ++request_id));
|
||||
};
|
||||
|
||||
while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) {
|
||||
ws->poll(100);
|
||||
|
||||
ws->dispatch([&](const std::string &message) {
|
||||
this->handle_message(ws, message, password, request_id, identified);
|
||||
});
|
||||
|
||||
if (ws->getReadyState() == WebSocket::CLOSED) {
|
||||
break;
|
||||
}
|
||||
|
||||
// nothing may be sent until the op 2 Identified handshake completes
|
||||
if (!identified) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// drain user commands
|
||||
std::deque<std::string> pending;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||
pending.swap(this->command_queue);
|
||||
}
|
||||
for (const auto &cmd : pending) {
|
||||
ws->send(build_request(cmd, ++request_id));
|
||||
}
|
||||
|
||||
// periodic status refresh (~1s) for live duration
|
||||
const auto now = steady_clock::now();
|
||||
if (now - last_status_poll >= milliseconds(1000)) {
|
||||
last_status_poll = now;
|
||||
request("GetStreamStatus");
|
||||
request("GetRecordStatus");
|
||||
}
|
||||
}
|
||||
|
||||
return identified;
|
||||
}
|
||||
|
||||
void OBSControl::worker_main() {
|
||||
|
||||
// connection settings are resolved once at launch into globals
|
||||
// (launcher.cpp, from the merged command-line + saved config options)
|
||||
if (!OBS_CONTROL_ENABLED) {
|
||||
log_info("obs", "disabled, not connecting");
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
|
||||
const std::string password = OBS_CONTROL_PASSWORD;
|
||||
|
||||
// opt easywsclient's internal diagnostics in/out per the debug option
|
||||
EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG;
|
||||
|
||||
// winsock is reference-counted: the app performs its own WSAStartup at
|
||||
// launch (which outlives this worker), so this paired Startup/Cleanup only
|
||||
// bumps the refcount and the WSACleanup below never tears down winsock for
|
||||
// the rest of the process
|
||||
WSADATA wsa_data;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
log_info("obs", "enabled, connecting to {}", url);
|
||||
|
||||
uint64_t request_id = 0;
|
||||
|
||||
// the reconnect loop retries every 5s; latch the auth-failure warning so a
|
||||
// wrong password logs once, not on every retry. reset after any identified
|
||||
// session so a later genuine failure is reported again
|
||||
bool auth_warning_logged = false;
|
||||
|
||||
// reconnect loop: keep a session alive while enabled, retrying on drop
|
||||
while (this->worker_running.load()) {
|
||||
|
||||
// mark "connecting" for the UI before each attempt
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.disabled = false;
|
||||
this->status.connected = false;
|
||||
this->status.identifying = true;
|
||||
this->status.connection_error.clear();
|
||||
}
|
||||
|
||||
// open the TCP socket and perform the WebSocket handshake; null means
|
||||
// OBS is unreachable (not running / wrong port / obs-websocket off)
|
||||
WebSocket::pointer ws = WebSocket::from_url(url);
|
||||
if (ws == nullptr) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.identifying = false;
|
||||
this->status.connection_error = "Unable to connect";
|
||||
}
|
||||
interruptible_sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// blocks here pumping the connection until it closes or we stop.
|
||||
// a session that never reaches "Identified" was rejected by OBS,
|
||||
// overwhelmingly because the password is wrong or missing
|
||||
const bool identified = this->run_session(ws, password, request_id);
|
||||
|
||||
// session ended: close the socket cleanly and free it
|
||||
ws->close();
|
||||
ws->poll();
|
||||
delete ws;
|
||||
|
||||
if (!identified && this->worker_running.load()) {
|
||||
if (!auth_warning_logged) {
|
||||
log_warning("obs", "connection closed before identify; "
|
||||
"OBS likely rejected authentication (check the password)");
|
||||
auth_warning_logged = true;
|
||||
}
|
||||
} else if (identified) {
|
||||
// a good session resets the latch so a future failure logs again
|
||||
auth_warning_logged = false;
|
||||
}
|
||||
|
||||
// connection dropped: clear live state so the UI doesn't show stale
|
||||
// scene/stream/record info while disconnected
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||
this->status.connected = false;
|
||||
this->status.identifying = false;
|
||||
if (this->status.connection_error.empty()) {
|
||||
this->status.connection_error =
|
||||
identified ? "Disconnected" : "Auth failed (check password)";
|
||||
}
|
||||
this->status.streaming = false;
|
||||
this->status.recording = false;
|
||||
this->status.record_paused = false;
|
||||
this->status.current_scene.clear();
|
||||
}
|
||||
|
||||
// clear any commands queued while disconnected
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||
this->command_queue.clear();
|
||||
}
|
||||
|
||||
// wait before reconnecting (interruptible)
|
||||
interruptible_sleep(5000);
|
||||
}
|
||||
|
||||
WSACleanup();
|
||||
log_info("obs", "OBS overlay worker stopped");
|
||||
}
|
||||
}
|
||||
@@ -876,8 +876,7 @@ namespace overlay::windows {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
// row hover detection (invisible selectable that spans entire row)
|
||||
ImGui::InvisibleTableRowSelectable();
|
||||
ImGui::HighlightTableRowOnHover();
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
@@ -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