mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cabd026ae | |||
| 7ccd938f9b | |||
| 88a210e82e | |||
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 | |||
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 | |||
| 2aae8ad9ca | |||
| 1b6d1af951 | |||
| 5c3617c05f | |||
| a9a943fbfe | |||
| b094a9e352 | |||
| 4bd18afb12 | |||
| 0b24e703a1 | |||
| 1a1be7a393 | |||
| a2d8f60d12 | |||
| 5d25fdb035 | |||
| c255e3a1db | |||
| d7c144646f | |||
| 27077aa21f | |||
| 632dcf7b41 | |||
| a25bbdc9c9 | |||
| 47d886306e | |||
| edda658fa4 | |||
| 1ab20f1b56 | |||
| 0e9093a74b | |||
| f52eaea76c | |||
| c3fcf2f417 | |||
| 8d04eba21d | |||
| 8d42c0ce77 | |||
| 829503568e | |||
| 746fdb5409 | |||
| 67da892fe9 | |||
| dc191487e0 | |||
| 5c0c405800 | |||
| 60166e6ba1 | |||
| cb0b59600a | |||
| b9f3be4df2 |
@@ -12,12 +12,12 @@ jobs:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Draft Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Draft Release
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Clean leftover build artifacts
|
||||
run: |
|
||||
rm -rf .ccache dist bin cmake-build-*
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- name: Determine release name from dist filename
|
||||
run: |
|
||||
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||
name="${dist%.zip}"
|
||||
# the tag is the date portion, e.g. 26-06-28
|
||||
tag="${name#spice2x-}"
|
||||
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
target_commitish: main
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
src/spice2x/dist/spice2x-*.zip
|
||||
+7
-3
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
||||
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
||||
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
||||
|
||||
### Forbidden "features" that will never be accepted
|
||||
### Pull requests that will not be accepted
|
||||
|
||||
* Performing decryption of encrypted files
|
||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
|
||||
* Performing decryption of encrypted files.
|
||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
|
||||
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
||||
* This includes automatic updaters.
|
||||
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
||||
* Built-in EA server that saves data. They should live in a separate project.
|
||||
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||
* Changes to the software license terms.
|
||||
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||
|
||||
### Avoiding regressions
|
||||
|
||||
|
||||
@@ -412,10 +412,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/sdvx/io.cpp
|
||||
games/sdvx/camera.cpp
|
||||
games/jb/jb.cpp
|
||||
games/jb/jb_touch.cpp
|
||||
games/jb/io.cpp
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/nost/touch_mode.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
@@ -431,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/rb/rb.cpp
|
||||
games/rb/io.cpp
|
||||
games/rb/touch.cpp
|
||||
games/rb/touch_debug.cpp
|
||||
games/bs/bs.cpp
|
||||
games/bs/io.cpp
|
||||
games/rf3d/rf3d.cpp
|
||||
@@ -500,6 +503,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
hooks/audio/xact.cpp
|
||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
@@ -526,6 +530,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/nvapi_impl.cpp
|
||||
hooks/graphics/nvapi_hook.cpp
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
@@ -569,7 +574,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -607,17 +611,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.cpp
|
||||
overlay/windows/midi.cpp
|
||||
overlay/windows/nostalgia_touch_piano.cpp
|
||||
overlay/windows/obs.cpp
|
||||
overlay/windows/obs_websocket.cpp
|
||||
overlay/windows/patch_manager.cpp
|
||||
overlay/windows/popn_sub.cpp
|
||||
overlay/windows/wnd_manager.cpp
|
||||
|
||||
# patcher
|
||||
patcher/config.cpp
|
||||
patcher/lifecycle.cpp
|
||||
patcher/loader.cpp
|
||||
patcher/loader_group.cpp
|
||||
patcher/runtime.cpp
|
||||
|
||||
# external
|
||||
external/easywsclient/easywsclient.cpp
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/rawinput_handles.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
rawinput/touch.cpp
|
||||
@@ -644,8 +658,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
stubs/stubs.cpp
|
||||
|
||||
# touch
|
||||
touch/gdi_overlay.cpp
|
||||
touch/native/inject.cpp
|
||||
touch/native/inject_mouse.cpp
|
||||
touch/native/inject_synthetic.cpp
|
||||
touch/native/nativetouchhook.cpp
|
||||
touch/native/transform.cpp
|
||||
touch/touch.cpp
|
||||
touch/touch_indicators.cpp
|
||||
touch/touch_gestures.cpp
|
||||
touch/win7.cpp
|
||||
touch/win8.cpp
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
// static stuff
|
||||
static int ACIO_WARMUP = 0;
|
||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
||||
// flush device output
|
||||
RI_MGR->devices_flush_output();
|
||||
|
||||
// update wintouchemu
|
||||
// update touch emulation
|
||||
wintouchemu::update();
|
||||
nativetouch::refresh_contact_lifetime();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/nost/io.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
#include "util/logging.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
||||
// static stuff
|
||||
static uint8_t STATUS_BUFFER[277];
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||
};
|
||||
|
||||
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||
|
||||
// iterate pairs of keys
|
||||
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
||||
|
||||
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button0 > 0) {
|
||||
state0 = button0;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
|
||||
state0 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state0 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
const auto button1 = panb_get_button_velocity(
|
||||
buttons.at(button_mapping[key_pair * 2 + 1]),
|
||||
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button1 > 0) {
|
||||
state1 = button1;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
|
||||
state1 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state1 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
// build value
|
||||
uint8_t value = 0;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "keypads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
||||
// get params
|
||||
auto keypad = req.params[0].GetUint();
|
||||
auto input = std::string(req.params[1].GetString());
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// process all chars
|
||||
for (auto c : input) {
|
||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,6 +10,8 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
@@ -17,6 +21,25 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool inject_native_touch(
|
||||
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||
if (touch_points.empty()) {
|
||||
// no active touches remain; release the synthetic contact
|
||||
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||
}
|
||||
|
||||
// only a single synthetic contact is supported, so just use the first touch point
|
||||
const auto &touch = touch_points.front();
|
||||
POINT position { touch.x, touch.y };
|
||||
if (canvas_w > 0 && canvas_h > 0) {
|
||||
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||
position,
|
||||
{ canvas_w, canvas_h },
|
||||
true);
|
||||
}
|
||||
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +49,25 @@ namespace api::modules {
|
||||
is_tdj_fhd = false;
|
||||
}
|
||||
|
||||
use_native = nativetouch::is_hooked();
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||
native_canvas_w = 0;
|
||||
native_canvas_h = 0;
|
||||
if (is_sdvx) {
|
||||
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
||||
native_canvas_w = 1080;
|
||||
native_canvas_h = 1920;
|
||||
} else if (avs::game::is_model("LDJ")) {
|
||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||
} else if (avs::game::is_model("M39")) {
|
||||
// pop'n music API touch surface
|
||||
native_canvas_w = 1280;
|
||||
native_canvas_h = 800;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,7 +141,11 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
touch_write_points(&touch_points);
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +169,43 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
touch_remove_points(&touch_point_ids);
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
touch_remove_points(&touch_point_ids);
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
const auto touch_id = touch_points.front().id;
|
||||
if (native_touch_id && *native_touch_id != touch_id) {
|
||||
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
return;
|
||||
}
|
||||
native_touch_id.reset();
|
||||
}
|
||||
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id = touch_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
if (!native_touch_id ||
|
||||
std::find(
|
||||
touch_point_ids.begin(),
|
||||
touch_point_ids.end(),
|
||||
*native_touch_id) == touch_point_ids.end()) {
|
||||
return;
|
||||
}
|
||||
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::apply_touch_errata(int &x, int &y) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
bool is_tdj_fhd;
|
||||
bool use_native;
|
||||
std::mutex native_touch_mutex;
|
||||
std::optional<uint32_t> native_touch_id;
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||
// used to normalize before mapping into the native injection window
|
||||
int native_canvas_w;
|
||||
int native_canvas_h;
|
||||
|
||||
// function definitions
|
||||
void read(Request &req, Response &res);
|
||||
void write(Request &req, Response &res);
|
||||
void write_reset(Request &req, Response &res);
|
||||
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||
void apply_touch_errata(int &x, int &y);
|
||||
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "controller.h"
|
||||
#include "serial.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
|
||||
@@ -17,7 +18,6 @@ namespace api {
|
||||
controller->init_state(this->state);
|
||||
this->thread = new std::thread([this] () {
|
||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// read buffer
|
||||
uint8_t read_buffer[16*1024];
|
||||
@@ -162,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
timer.sleep(retry_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1924,7 +1924,7 @@ namespace avs {
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
property_search(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
|
||||
@@ -214,6 +214,19 @@ time (
|
||||
echo "==========================="
|
||||
)
|
||||
|
||||
echo ""
|
||||
echo "Checking Win7 compatibility..."
|
||||
echo "============================="
|
||||
if ! command -v windows_dll_compat_checker &> /dev/null; then
|
||||
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
|
||||
else
|
||||
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
|
||||
${BUILDDIR_64}/spicetools/64/spice64.exe
|
||||
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
|
||||
${BUILDDIR_32}/spicetools/spicecfg.exe \
|
||||
${BUILDDIR_32}/spicetools/32/spice.exe
|
||||
fi
|
||||
|
||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||
then
|
||||
echo ""
|
||||
@@ -229,7 +242,7 @@ then
|
||||
fi
|
||||
if ((BUILD_XP_32 > 0))
|
||||
then
|
||||
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
|
||||
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
|
||||
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
||||
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
||||
fi
|
||||
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||
mkdir -p ${OUTDIR_EXTRAS}/updater
|
||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||
then
|
||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
|
||||
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
||||
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
||||
|
||||
# generate the standalone updater scripts from the shared template and drop them
|
||||
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
|
||||
|
||||
# build distribution archive
|
||||
if ((DIST_ENABLE > 0))
|
||||
then
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# builds the standalone updater scripts from the shared PowerShell logic in
|
||||
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
|
||||
# header (with the release channel baked in) followed by the shared PowerShell
|
||||
# body after the marker line.
|
||||
#
|
||||
# usage: build_updaters.sh OUT_DIR
|
||||
# OUT_DIR directory to write the generated .bat files into
|
||||
|
||||
set -eu
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "usage: build_updaters.sh OUT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PS_BODY="${SRC_DIR}/update_spice.ps1"
|
||||
OUT_DIR="$1"
|
||||
|
||||
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
|
||||
|
||||
MARKER='#___PS___'
|
||||
|
||||
gen() {
|
||||
channel="$1"; label="$2"; out="$3"
|
||||
{
|
||||
cat <<HEADER
|
||||
@echo off
|
||||
setlocal
|
||||
REM ============================================================================
|
||||
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
|
||||
REM this folder to the latest spice2x release from GitHub.
|
||||
REM
|
||||
REM release channel: ${label}
|
||||
REM make sure spice/spicecfg are NOT running before updating.
|
||||
REM ============================================================================
|
||||
|
||||
REM expose the script folder + channel to the embedded PowerShell, then run the
|
||||
REM PowerShell part: re-read this file and execute everything after the marker.
|
||||
set "SPICE_DIR=%~dp0"
|
||||
set "SPICE_CHANNEL=${channel}"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
|
||||
set "RC=%ERRORLEVEL%"
|
||||
endlocal & exit /b %RC%
|
||||
|
||||
${MARKER}
|
||||
HEADER
|
||||
cat "$PS_BODY"
|
||||
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
|
||||
echo " generated ${OUT_DIR}/${out}"
|
||||
}
|
||||
|
||||
echo "Generating updater scripts from $(basename "$PS_BODY")..."
|
||||
gen "" "stable (pre-releases excluded)" "update_spice.bat"
|
||||
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
|
||||
+72
-11
@@ -3,7 +3,9 @@
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/piuio.h"
|
||||
#include "util/time.h"
|
||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
||||
namespace GameAPI::Buttons {
|
||||
static State get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &button,
|
||||
bool check_alts,
|
||||
bool check_modifiers);
|
||||
|
||||
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
|
||||
}
|
||||
|
||||
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
|
||||
const auto modifier_mask = button.getModifierMask();
|
||||
if (modifier_mask == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
|
||||
if (!modifier_buttons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
|
||||
(index >= modifier_buttons->size() ||
|
||||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
|
||||
GameAPI::Buttons::BUTTON_PRESSED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &_button,
|
||||
bool check_alts,
|
||||
bool check_modifiers) {
|
||||
|
||||
// check override
|
||||
if (_button.override_enabled) {
|
||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
std::optional<bool> window_has_focus;
|
||||
while (true) {
|
||||
|
||||
// skip bindings whose required modifiers are not held
|
||||
//
|
||||
// note that modifiers cannot process MIDI as the logic is written
|
||||
// below, since MIDI buttons are event-based (on event, off event, etc)
|
||||
// and cannot be correctly handled by a simple early return
|
||||
// there is no explicit check for MIDI here, but the UI should have
|
||||
// prevented it
|
||||
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
|
||||
button_count++;
|
||||
if (!alternatives || alternatives->empty() ||
|
||||
button_count - 1 >= alternatives->size()) {
|
||||
return BUTTON_NOT_PRESSED;
|
||||
}
|
||||
current_button = &alternatives->at(button_count - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (current_button->isNaive()) {
|
||||
GameAPI::Buttons::State state;
|
||||
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
}
|
||||
case rawinput::DESTROYED:
|
||||
// device was unplugged. release the button instead of leaving
|
||||
// state at getLastState(), which would latch a held button on
|
||||
// forever (e.g. a MIDI note held while the device is removed)
|
||||
state = BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -463,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
|
||||
return get_button_state(manager, button, check_alts, true);
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
||||
if (manager) {
|
||||
return getState(manager.get(), button, check_alts);
|
||||
@@ -478,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
return button.override_velocity;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (button.isNaive()) {
|
||||
if (button.getInvert()) {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
||||
} else {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// get button state
|
||||
Buttons::State button_state = Buttons::getState(manager, button, false);
|
||||
const auto button_state = Buttons::getState(manager, button, false);
|
||||
|
||||
// naive bindings report their digital state as full or zero velocity
|
||||
if (button.isNaive()) {
|
||||
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
|
||||
}
|
||||
|
||||
// check if button isn't being pressed
|
||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||
|
||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
||||
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
||||
}
|
||||
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
|
||||
auto binding_display = this->getBindingDisplayString(manager);
|
||||
auto modifiers = this->getModifierMask();
|
||||
if (binding_display.empty() || modifiers == 0) {
|
||||
return binding_display;
|
||||
}
|
||||
|
||||
std::string modifier_display;
|
||||
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
|
||||
if ((modifiers & UINT8_C(1)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!modifier_display.empty()) {
|
||||
modifier_display += "+";
|
||||
}
|
||||
modifier_display += fmt::format("Mod{}", index);
|
||||
}
|
||||
return modifier_display + "+" + binding_display;
|
||||
}
|
||||
|
||||
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
|
||||
|
||||
// get VKey string
|
||||
auto vKey = (uint16_t) this->getVKey();
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
std::string name;
|
||||
std::string device_identifier = "";
|
||||
unsigned short vKey = INVALID_VKEY;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
// default bindings are always naive
|
||||
unsigned short vKey_default = INVALID_VKEY;
|
||||
@@ -65,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -123,6 +126,14 @@ public:
|
||||
return this->name;
|
||||
}
|
||||
|
||||
inline uint8_t getModifierMask() const {
|
||||
return this->modifier_mask;
|
||||
}
|
||||
|
||||
inline void setModifierMask(uint8_t new_modifier_mask) {
|
||||
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
|
||||
}
|
||||
|
||||
inline const std::string &getDeviceIdentifier() const {
|
||||
return this->device_identifier;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||
gameButtonNode = this->configFile.NewElement("button");
|
||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
} else {
|
||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
|
||||
// find alternative
|
||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
alt.setDeviceIdentifier(std::string(devid));
|
||||
}
|
||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("bat_threshold", &bat);
|
||||
entry.bat_threshold = bat;
|
||||
|
||||
unsigned int modifier_mask = 0;
|
||||
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -160,6 +165,28 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
|
||||
if (mod_el) {
|
||||
auto *btn_el = mod_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.modifier_buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.modifier_buttons.push_back(
|
||||
{btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights) {
|
||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
||||
}
|
||||
tmpl_el->InsertEndChild(buttons_el);
|
||||
|
||||
// modifier buttons - skip unbound entries
|
||||
if (!tmpl.modifier_buttons.empty()) {
|
||||
auto *mod_el = doc.NewElement("modifier_buttons");
|
||||
if (save_modifiers) {
|
||||
for (auto &btn : tmpl.modifier_buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(mod_el);
|
||||
}
|
||||
|
||||
// keypad buttons — skip unbound entries
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
|
||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||
{
|
||||
pwd_ = absolute(pwd);
|
||||
// spice2x
|
||||
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||
// prefixes, which makes directory_iterator() throw during startup
|
||||
// spice2x
|
||||
const auto &nativePwd = pwd.native();
|
||||
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||
UpdateFileRecords();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "trackball.h"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
@@ -653,9 +654,13 @@ namespace games::gitadora {
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||
std::string mediaType = mediaTypePointer->GetString();
|
||||
if (mediaType.length() > 0) {
|
||||
camera->m_selectedMediaTypeDescription = mediaType;
|
||||
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||
camera->m_useAutoMediaType = false;
|
||||
} else {
|
||||
camera->m_useAutoMediaType = true;
|
||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
||||
std::string drawModeString = drawModePointer->GetString();
|
||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
||||
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
||||
// Flip
|
||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
||||
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||
}
|
||||
|
||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
||||
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||
}
|
||||
|
||||
// Allow manual control
|
||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
||||
if (camera->m_useAutoMediaType) {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||
} else {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
||||
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||
}
|
||||
|
||||
// Draw Mode
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
|
||||
const auto drawMode = camera->m_drawMode.load();
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
|
||||
|
||||
// Flip
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||
|
||||
// Manual control
|
||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "launcher/options.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/fileutils.h"
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "bi2x_hook.h"
|
||||
#include "ezusb.h"
|
||||
#include "io.h"
|
||||
#include "poke.h"
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
@@ -63,7 +64,8 @@ namespace games::iidx {
|
||||
bool TDJ_MODE = false;
|
||||
bool FORCE_720P = false;
|
||||
bool DISABLE_ESPEC_IO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
bool ENABLE_POKE = false;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
@@ -350,12 +352,15 @@ namespace games::iidx {
|
||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||
devicehook_init(avs::core::DLL_INSTANCE);
|
||||
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
|
||||
wintouchemu::hook_title_ends(
|
||||
"beatmania IIDX",
|
||||
"main",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +538,12 @@ namespace games::iidx {
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::post_attach() {
|
||||
if (ENABLE_POKE) {
|
||||
poke::enable();
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace games::iidx {
|
||||
extern bool FORCE_720P;
|
||||
extern bool DISABLE_ESPEC_IO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
extern bool ENABLE_POKE;
|
||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern uint8_t DIGITAL_TT_SENS;
|
||||
@@ -53,6 +54,7 @@ namespace games::iidx {
|
||||
|
||||
virtual void attach() override;
|
||||
virtual void pre_attach() override;
|
||||
virtual void post_attach() override;
|
||||
virtual void detach() override;
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
#include <atomic>
|
||||
#include <d3d9.h>
|
||||
#include <dxva2api.h>
|
||||
#include <mutex>
|
||||
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
|
||||
extern std::string DRAW_MODE_LABELS[];
|
||||
|
||||
namespace games::iidx {
|
||||
class IIDXCameraSourceReaderCallback;
|
||||
|
||||
namespace Camera {
|
||||
struct PlayVideoCamera {
|
||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
||||
@@ -87,7 +90,7 @@ namespace games::iidx {
|
||||
|
||||
class IIDXLocalCamera {
|
||||
protected:
|
||||
virtual ~IIDXLocalCamera() {};
|
||||
virtual ~IIDXLocalCamera();
|
||||
|
||||
LONG m_nRefCount;
|
||||
CRITICAL_SECTION m_critsec;
|
||||
@@ -101,9 +104,14 @@ namespace games::iidx {
|
||||
// For reading frames from Camera
|
||||
IMFMediaSource *m_pSource = nullptr;
|
||||
IMFSourceReader *m_pSourceReader = nullptr;
|
||||
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
|
||||
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
|
||||
std::mutex m_mediaTypeMutex;
|
||||
IMFMediaType *m_pendingMediaType = nullptr;
|
||||
int m_selectedMediaTypeIndex = 0;
|
||||
std::string m_selectedMediaTypeDescription = "";
|
||||
|
||||
// Camera Format information
|
||||
double m_frameRate = 0;
|
||||
LONG m_cameraWidth;
|
||||
LONG m_cameraHeight;
|
||||
|
||||
@@ -129,6 +137,12 @@ namespace games::iidx {
|
||||
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
||||
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
||||
|
||||
// upload surface for decoded camera frames returned in system memory
|
||||
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
|
||||
GUID m_decodedSubtype = GUID_NULL;
|
||||
GUID m_outputSubtype = GUID_NULL;
|
||||
bool m_drawErrorLogged = false;
|
||||
|
||||
// Texture for custom transform (e.g. horizontal flip)
|
||||
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
||||
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
||||
@@ -153,21 +167,19 @@ namespace games::iidx {
|
||||
BOOL m_initialized = false;
|
||||
|
||||
// True if all the setup steps succeeded
|
||||
BOOL m_active = false;
|
||||
std::atomic_bool m_active = false;
|
||||
|
||||
// Media type select
|
||||
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
||||
int m_selectedMediaTypeIndex = 0;
|
||||
bool m_useAutoMediaType = true;
|
||||
IMFMediaType *m_pAutoMediaType = nullptr;
|
||||
std::string m_selectedMediaTypeDescription = "";
|
||||
bool m_allowManualControl = false;
|
||||
|
||||
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
|
||||
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
|
||||
|
||||
// Render processing
|
||||
bool m_flipHorizontal = false;
|
||||
bool m_flipVertical = false;
|
||||
std::atomic_bool m_flipHorizontal = false;
|
||||
std::atomic_bool m_flipVertical = false;
|
||||
|
||||
IIDXLocalCamera(
|
||||
std::string name,
|
||||
@@ -184,11 +196,14 @@ namespace games::iidx {
|
||||
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
||||
HRESULT SetCameraControlProp(int index, long value, long flags);
|
||||
HRESULT ResetCameraControlProps();
|
||||
HRESULT FlushDrawCommands();
|
||||
std::string GetName();
|
||||
std::string GetFriendlyName();
|
||||
std::string GetSymLink();
|
||||
int GetSelectedMediaTypeIndex();
|
||||
std::string GetSelectedMediaTypeDescription();
|
||||
void SetSelectedMediaTypeDescription(const std::string &description);
|
||||
HRESULT ChangeMediaType(IMFMediaType *pType);
|
||||
void RequestMediaType(IMFMediaType *pType);
|
||||
HRESULT StartCapture();
|
||||
void UpdateDrawRect();
|
||||
|
||||
@@ -197,12 +212,22 @@ namespace games::iidx {
|
||||
void CreateThread();
|
||||
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
||||
std::string GetVideoFormatName(GUID subtype);
|
||||
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
|
||||
static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
|
||||
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
|
||||
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo ¤t);
|
||||
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
|
||||
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
|
||||
HRESULT ValidateMediaType(IMFMediaType *pType);
|
||||
HRESULT InitTargetTexture();
|
||||
HRESULT EnsureTransformTextures();
|
||||
HRESULT InitCameraControl();
|
||||
void SetSelectedMediaType(int index, const std::string &description);
|
||||
bool HasPendingMediaType();
|
||||
HRESULT ApplyPendingMediaType();
|
||||
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT ReadSample();
|
||||
LPDIRECT3DTEXTURE9 Render();
|
||||
HRESULT Render();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace games::iidx {
|
||||
_In_ UINT32 cInitialSize
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
|
||||
_Out_ IMFMediaType** ppMFType
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
@@ -35,6 +39,7 @@ namespace games::iidx {
|
||||
);
|
||||
|
||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||
static MFCreateMediaType_t MFCreateMediaType = nullptr;
|
||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||
static MFGetService_t MFGetService = nullptr;
|
||||
@@ -66,6 +71,12 @@ namespace games::iidx {
|
||||
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
||||
}
|
||||
|
||||
MFCreateMediaType = (MFCreateMediaType_t)
|
||||
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
|
||||
if (!MFCreateMediaType) {
|
||||
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
|
||||
}
|
||||
|
||||
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||
if (!MFEnumDeviceSources) {
|
||||
@@ -92,6 +103,11 @@ namespace games::iidx {
|
||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType) {
|
||||
return MFCreateMediaType(ppMFType);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace games::iidx {
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize);
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType);
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
|
||||
+26
-147
@@ -1,31 +1,17 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "windows.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
#include "games/io.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/windows/generic_sub.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
#define POKE_NATIVE_TOUCH 0
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
|
||||
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
|
||||
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
|
||||
static InjectTouchInput_t pInjectTouchInput = nullptr;
|
||||
#endif
|
||||
|
||||
namespace games::iidx::poke {
|
||||
|
||||
@@ -116,93 +102,29 @@ namespace games::iidx::poke {
|
||||
{"1/D", 50},
|
||||
};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
void initialize_native_touch_library() {
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||
}
|
||||
|
||||
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
|
||||
USER32_INSTANCE, "InitializeTouchInjection");
|
||||
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
|
||||
USER32_INSTANCE, "InjectTouchInput");
|
||||
}
|
||||
|
||||
void emulate_native_touch(TouchPoint tp, bool is_down) {
|
||||
if (pInjectTouchInput == nullptr) {
|
||||
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINTER_TOUCH_INFO contact;
|
||||
BOOL bRet = TRUE;
|
||||
const int contact_offset = 2;
|
||||
|
||||
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
|
||||
|
||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
||||
contact.pointerInfo.pointerId = 0;
|
||||
contact.pointerInfo.ptPixelLocation.x = tp.x;
|
||||
contact.pointerInfo.ptPixelLocation.y = tp.y;
|
||||
if (is_down) {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
||||
} else {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
|
||||
}
|
||||
|
||||
contact.pointerInfo.dwTime = 0;
|
||||
contact.pointerInfo.PerformanceCount = 0;
|
||||
|
||||
contact.touchFlags = TOUCH_FLAG_NONE;
|
||||
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
|
||||
contact.orientation = 0;
|
||||
contact.pressure = 1024;
|
||||
|
||||
contact.rcContact.top = tp.y - contact_offset;
|
||||
contact.rcContact.bottom = tp.y + contact_offset;
|
||||
contact.rcContact.left = tp.x - contact_offset;
|
||||
contact.rcContact.right = tp.x + contact_offset;
|
||||
|
||||
bRet = InjectTouchInput(1, &contact);
|
||||
if (!bRet) {
|
||||
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
|
||||
}
|
||||
const auto &touch = touch_points.front();
|
||||
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||
}
|
||||
|
||||
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
int i = 0;
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, true);
|
||||
}
|
||||
}
|
||||
|
||||
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, false);
|
||||
}
|
||||
touch_points->clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
}
|
||||
|
||||
void enable() {
|
||||
|
||||
// check if already running
|
||||
if (THREAD)
|
||||
return;
|
||||
|
||||
if (!games::iidx::NATIVE_TOUCH) {
|
||||
log_warning("poke", "keypad touch emulation requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
@@ -210,35 +132,13 @@ namespace games::iidx::poke {
|
||||
std::vector<TouchPoint> touch_points;
|
||||
std::vector<uint16_t> last_keypad_state = {0, 0};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
bool use_native = games::iidx::NATIVE_TOUCH;
|
||||
|
||||
if (use_native) {
|
||||
initialize_native_touch_library();
|
||||
|
||||
if (pInitializeTouchInjection != nullptr) {
|
||||
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
bool use_native = false;
|
||||
#endif
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
|
||||
// clean up touch from last frame
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
clear_native_touch_points(&touch_points);
|
||||
} else {
|
||||
clear_touch_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
clear_touch_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, false);
|
||||
touch_points.clear();
|
||||
}
|
||||
|
||||
for (int unit = 0; unit < 2; unit++) {
|
||||
@@ -263,32 +163,15 @@ namespace games::iidx::poke {
|
||||
|
||||
float x = x_iter->second / 1920.0;
|
||||
float y = y_iter->second / 1080.0;
|
||||
if (use_native) {
|
||||
x *= rawinput::TOUCHSCREEN_RANGE_X;
|
||||
y *= rawinput::TOUCHSCREEN_RANGE_Y;
|
||||
} else if (GRAPHICS_IIDX_WSUB) {
|
||||
// Scale to windowed subscreen
|
||||
x *= GRAPHICS_WSUB_WIDTH;
|
||||
y *= GRAPHICS_WSUB_HEIGHT;
|
||||
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
|
||||
// Overlay is not present, scale to main screen
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
x *= ImGui::GetIO().DisplaySize.x;
|
||||
y *= ImGui::GetIO().DisplaySize.y;
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
continue;
|
||||
}
|
||||
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
// Overlay subscreen
|
||||
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
|
||||
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
|
||||
|
||||
// Scale to window size ratio
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
|
||||
}
|
||||
x = x_iter->second;
|
||||
y = y_iter->second;
|
||||
}
|
||||
|
||||
TouchPoint tp {
|
||||
@@ -309,19 +192,15 @@ namespace games::iidx::poke {
|
||||
} // for all units
|
||||
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
emulate_native_touch_points(&touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
touch_write_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, true);
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(50);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
inject_native_touch_points(touch_points, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace games {
|
||||
static bool IO_INITIALIZED = false;
|
||||
static std::vector<std::string> games;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
|
||||
static robin_hood::unordered_map<std::string, std::string> buttons_help;
|
||||
@@ -365,6 +366,37 @@ namespace games {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
|
||||
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
|
||||
const std::vector<std::string> names {
|
||||
"Modifier 1",
|
||||
"Modifier 2",
|
||||
"Modifier 3",
|
||||
"Modifier 4",
|
||||
};
|
||||
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
|
||||
for (auto &modifier : modifier_buttons) {
|
||||
modifier.setModifierMask(0);
|
||||
for (auto &alternative : modifier.getAlternatives()) {
|
||||
alternative.setModifierMask(0);
|
||||
}
|
||||
}
|
||||
return modifier_buttons;
|
||||
}
|
||||
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
|
||||
initialize();
|
||||
auto it = buttons_modifiers.find(game);
|
||||
if (it == buttons_modifiers.end()) {
|
||||
if (game.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
buttons_modifiers[game] = gen_buttons_modifiers(game);
|
||||
return &buttons_modifiers[game];
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
|
||||
auto buttons = GameAPI::Buttons::getButtons(game);
|
||||
std::vector<std::string> names;
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
namespace games {
|
||||
|
||||
namespace ModifierButtons {
|
||||
enum {
|
||||
Modifier1,
|
||||
Modifier2,
|
||||
Modifier3,
|
||||
Modifier4,
|
||||
Size,
|
||||
};
|
||||
}
|
||||
|
||||
namespace OverlayButtons {
|
||||
enum {
|
||||
Screenshot,
|
||||
@@ -59,6 +69,7 @@ namespace games {
|
||||
|
||||
const std::vector<std::string> &get_games();
|
||||
std::vector<Button> *get_buttons(const std::string &game);
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game);
|
||||
std::string get_buttons_help(const std::string &game);
|
||||
std::string get_analogs_help(const std::string &game);
|
||||
std::vector<Button> *get_buttons_keypads(const std::string &game);
|
||||
|
||||
+5
-210
@@ -3,212 +3,24 @@
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_BUTTON_GAP 37
|
||||
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch stuff
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
static bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::vector<TouchPoint> TOUCH_POINTS;
|
||||
bool TOUCH_STATE[16];
|
||||
|
||||
void touch_update() {
|
||||
|
||||
// check if touch enabled
|
||||
if (!TOUCH_ENABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch module
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
/*
|
||||
* Find the game window.
|
||||
* We check the foreground window first, then fall back to searching for the window title
|
||||
* All game versions seem to have their model first in the window title
|
||||
*/
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
|
||||
// check if we have a window handle
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch hook
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// show cursor
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
// earlier games use a different screen orientation
|
||||
if (!avs::game::is_model("L44")) {
|
||||
IS_PORTRAIT = false;
|
||||
}
|
||||
|
||||
// set attached
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// reset touch state
|
||||
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
|
||||
|
||||
// check touch points
|
||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
||||
TOUCH_POINTS.clear();
|
||||
touch_get_points(TOUCH_POINTS);
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
|
||||
// get grid coordinates
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
|
||||
// set the corresponding state
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
// check window out of bounds
|
||||
if (IS_PORTRAIT) {
|
||||
if (tp.x > 768 || tp.y > 1360) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (tp.x > 1360 || tp.y > 768) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int x_relative = tp.x;
|
||||
int y_relative = tp.y;
|
||||
|
||||
// x_relative and y_relative are relative to the top-left pixel of the first button
|
||||
if (IS_PORTRAIT) {
|
||||
// which is at (8, 602) in portrait:
|
||||
// X: [8...759] (752 pixels wide)
|
||||
// Y: [602...1353] (752 pixels high)
|
||||
x_relative -= 8;
|
||||
y_relative -= 602;
|
||||
} else {
|
||||
// and at (8, 8) in landscape
|
||||
x_relative -= 8;
|
||||
y_relative -= 8;
|
||||
}
|
||||
|
||||
int x_index = -1;
|
||||
int x_hitbox = 0;
|
||||
int y_index = -1;
|
||||
int y_hitbox = 0;
|
||||
|
||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
||||
if (x_relative >= 0) {
|
||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
if (y_relative >= 0) {
|
||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
|
||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
||||
|
||||
if (TOUCH_ALGORITHM == Improved) {
|
||||
if (IS_PORTRAIT) {
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// right and bottom borders are covered by the hit box
|
||||
} else {
|
||||
// extend to top border
|
||||
if (y_relative < 0) {
|
||||
y_index = 0;
|
||||
}
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// bottom border is covered by the hit box
|
||||
// rightmost edge - ignore them entirely
|
||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the gap was touched
|
||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
||||
// in ac-accurate mode, touching the gap is ignored
|
||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (TOUCH_ALGORITHM == Improved) {
|
||||
// in improved mode, touching the gap triggers the closest button
|
||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
||||
x_index++;
|
||||
}
|
||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
||||
y_index++;
|
||||
}
|
||||
}
|
||||
|
||||
// set the corresponding state
|
||||
int index = y_index * 4 + x_index;
|
||||
if (0 <= index && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
|
||||
*/
|
||||
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||
static BOOL __stdcall network_addr_is_changed() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
|
||||
*/
|
||||
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
|
||||
static BOOL __stdcall network_get_network_check_info() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix network error on non DHCP interfaces
|
||||
*/
|
||||
// fixes network errors on non-DHCP interfaces
|
||||
static BOOL __cdecl network_get_dhcp_result() {
|
||||
return 1;
|
||||
}
|
||||
@@ -248,23 +60,7 @@ namespace games::jb {
|
||||
void JBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
// enable touch
|
||||
TOUCH_ENABLE = true;
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
||||
break;
|
||||
}
|
||||
touch_attach();
|
||||
|
||||
// enable debug logging of gftools
|
||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||
@@ -284,7 +80,6 @@ namespace games::jb {
|
||||
void JBGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
// disable touch
|
||||
TOUCH_ENABLE = false;
|
||||
touch_detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "games/game.h"
|
||||
#include "games/jb/jb_touch.h"
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// touch stuff
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern bool TOUCH_STATE[16];
|
||||
void touch_update();
|
||||
|
||||
class JBGame : public games::Game {
|
||||
public:
|
||||
JBGame();
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
#include "jb_touch.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
|
||||
// (752px across). the first button's top-left is at (8, 602) in portrait and
|
||||
// (6, 8) in landscape.
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_MAX_BUTTON_GAP 38
|
||||
|
||||
// improved and plus modes use this reach around each button. must be >= the
|
||||
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||
#define JB_TOUCH_RADIUS 38
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
static_assert(std::atomic_bool::is_always_lock_free);
|
||||
static_assert(std::atomic_uint16_t::is_always_lock_free);
|
||||
static_assert(std::atomic<POINT>::is_always_lock_free);
|
||||
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
|
||||
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
|
||||
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
|
||||
|
||||
// touch state
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
|
||||
uint32_t TOUCH_DEBOUNCE_MS = 0;
|
||||
static std::atomic_bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||
|
||||
// fixed-size contact view used by the debug overlay
|
||||
static const size_t JB_MAX_TOUCH_POINTS = 16;
|
||||
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
|
||||
static constexpr POINT JB_INVALID_TOUCH_POINT {
|
||||
JB_INVALID_TOUCH_COORD,
|
||||
JB_INVALID_TOUCH_COORD
|
||||
};
|
||||
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
|
||||
|
||||
static void clear_touch_points() {
|
||||
for (auto &point : TOUCH_POINTS) {
|
||||
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// false when a contact is younger than the debounce window and should be ignored
|
||||
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
|
||||
return now_ms - tp.down_ms >= threshold_ms;
|
||||
}
|
||||
|
||||
// snapshot for the debug overlay: each slot atomically holds one matured contact or
|
||||
// the invalid sentinel. immature and absent contacts are published as invalid, so the
|
||||
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
|
||||
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
|
||||
double now_ms, double threshold_ms) {
|
||||
size_t count = touch_points.size();
|
||||
if (count > JB_MAX_TOUCH_POINTS) {
|
||||
count = JB_MAX_TOUCH_POINTS;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
|
||||
POINT point = JB_INVALID_TOUCH_POINT;
|
||||
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
|
||||
point = { touch_points[i].x, touch_points[i].y };
|
||||
}
|
||||
TOUCH_POINTS[i].store(point, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// --- touch geometry ------------------------------------------------------
|
||||
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||
|
||||
struct AxisGeometry {
|
||||
int button[4]; // left/top edge of each button
|
||||
};
|
||||
|
||||
// left/top edges of the four buttons along one axis, starting at `first`
|
||||
static AxisGeometry axis_geometry(int first) {
|
||||
AxisGeometry g {};
|
||||
g.button[0] = first;
|
||||
for (int i = 1; i < 4; i++) {
|
||||
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// button edges for the current orientation
|
||||
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
||||
if (IS_PORTRAIT) {
|
||||
gx = axis_geometry(8);
|
||||
gy = axis_geometry(602);
|
||||
} else {
|
||||
gx = axis_geometry(6);
|
||||
gy = axis_geometry(8);
|
||||
}
|
||||
}
|
||||
|
||||
// distance from `p` to a button along one axis (0 when inside)
|
||||
static int axis_distance(int p, int button) {
|
||||
int end = button + JB_BUTTON_SIZE - 1;
|
||||
if (p < button) {
|
||||
return button - p;
|
||||
}
|
||||
if (p > end) {
|
||||
return p - end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
|
||||
// shared by detection and the debug overlay so both agree which button a touch hits
|
||||
static int nearest_button(
|
||||
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
|
||||
int best_index = -1;
|
||||
int best_dist = 0;
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
int dist = dx * dx + dy * dy;
|
||||
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||
best_dist = dist;
|
||||
best_index = r * 4 + c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
// detection reach for the current algorithm (0 = register only inside a button)
|
||||
static int touch_radius() {
|
||||
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
|
||||
}
|
||||
|
||||
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||
// every button within `radius` when `multi` (edge/gap presses trigger several)
|
||||
static void mark_buttons(uint16_t &state, int px, int py,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
int radius, bool multi) {
|
||||
if (!multi) {
|
||||
int index = nearest_button(px, py, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
state |= uint16_t(1) << index;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
if (dx * dx + dy * dy <= radius * radius) {
|
||||
state |= uint16_t(1) << (r * 4 + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::bitset<16> touch_state() {
|
||||
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
void touch_update() {
|
||||
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// one-time touch window attach
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
// find the game window: prefer the foreground window, else search by
|
||||
// title (the model name prefixes the window title in every version)
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
// only the L44 model runs in portrait; set this before starting the
|
||||
// touch-window thread so the renderer only observes the final value
|
||||
IS_PORTRAIT = avs::game::is_model("L44");
|
||||
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
|
||||
// let the rawinput stack correct the aspect ratio
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// calculate the next state locally and publish it after processing every touch
|
||||
uint16_t next_state = 0;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
touch_get_points(touch_points);
|
||||
|
||||
// debounce: the landing time is stamped by the touch layer, so this is
|
||||
// independent of how often the game polls for input (0 = off)
|
||||
double now_ms = get_performance_milliseconds();
|
||||
double threshold_ms = TOUCH_DEBOUNCE_MS;
|
||||
|
||||
// publish every raw contact (with its maturity) for the debug overlay, then drop
|
||||
// the ones still inside the debounce window so no algorithm sees them
|
||||
publish_touch_points(touch_points, now_ms, threshold_ms);
|
||||
std::erase_if(touch_points, [&](const TouchPoint &tp) {
|
||||
return !touch_matured(tp, now_ms, threshold_ms);
|
||||
});
|
||||
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
|
||||
// legacy: evenly divide the play area into a 4x4 grid
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : touch_points) {
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
next_state |= uint16_t(1) << index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// accurate registers only a touch inside a button; improved snaps each touch
|
||||
// to the single nearest button within reach (so a gap or centre touch still
|
||||
// triggers exactly one button); plus marks every button within the same reach,
|
||||
// so an edge or gap touch can trigger several at once (like the mobile game)
|
||||
AxisGeometry gx, gy;
|
||||
touch_geometry(gx, gy);
|
||||
int radius = touch_radius();
|
||||
bool multi = (TOUCH_ALGORITHM == Plus);
|
||||
for (auto &tp : touch_points) {
|
||||
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
|
||||
}
|
||||
}
|
||||
|
||||
TOUCH_STATE.store(next_state, std::memory_order_release);
|
||||
}
|
||||
|
||||
// true when any supported touch handler detects a touchscreen (checked once)
|
||||
static bool touchscreen_detected() {
|
||||
static const bool detected = is_touch_available("jubeat touch debug overlay");
|
||||
return detected;
|
||||
}
|
||||
|
||||
// auto draws the boxes only when a touch screen is present
|
||||
static bool debug_show_boxes() {
|
||||
switch (TOUCH_DEBUG_OVERLAY) {
|
||||
case JbTouchDebugBox:
|
||||
case JbTouchDebugAll:
|
||||
return true;
|
||||
case JbTouchDebugAuto:
|
||||
return touchscreen_detected();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool debug_show_taps() {
|
||||
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
|
||||
}
|
||||
|
||||
bool touch_debug_overlay_enabled() {
|
||||
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
|
||||
(debug_show_boxes() || debug_show_taps());
|
||||
}
|
||||
|
||||
// the 16 boundary rects: legacy divides the area evenly, others use button squares
|
||||
static void debug_cells(
|
||||
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
|
||||
if (legacy) {
|
||||
int offset = IS_PORTRAIT ? 580 : 0;
|
||||
int x_edges[5];
|
||||
int y_edges[5];
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
x_edges[i] = i * 768 / 4;
|
||||
y_edges[i] = offset + i * (1360 - 580) / 4;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = {
|
||||
gx.button[c], gy.button[r],
|
||||
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
|
||||
static void draw_debug_boxes(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
RECT cells[16];
|
||||
debug_cells(legacy, gx, gy, cells);
|
||||
|
||||
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
|
||||
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
|
||||
auto state = touch_state();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
|
||||
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
|
||||
SelectObject(hdc, old_pen);
|
||||
}
|
||||
DeleteObject(pen_idle);
|
||||
DeleteObject(pen_active);
|
||||
}
|
||||
|
||||
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
|
||||
// reaches across gaps to nearby buttons, and the others only fire inside a button square
|
||||
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
bool legacy, int radius) {
|
||||
if (legacy) {
|
||||
return true;
|
||||
}
|
||||
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
|
||||
return nearest_button(px, py, gx, gy, reach) >= 0;
|
||||
}
|
||||
|
||||
// 90-degree arc on the circle rim facing the centre of button `index`
|
||||
static void draw_tap_arc(HDC hdc, int px, int py, int index,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||
int c = index % 4;
|
||||
int r = index / 4;
|
||||
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
||||
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
||||
const double quarter = 3.14159265358979323846 / 2.0;
|
||||
const int segments = 16;
|
||||
POINT arc[segments + 1];
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
double a = mid - quarter / 2.0 + quarter * i / segments;
|
||||
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
|
||||
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
|
||||
}
|
||||
Polyline(hdc, arc, segments + 1);
|
||||
}
|
||||
|
||||
// circle at each touch: white when it presses a button, grey otherwise; in single-button
|
||||
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
|
||||
// plus, where a touch can trigger several buttons at once)
|
||||
static void draw_debug_taps(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
// PS_INSIDEFRAME keeps the stroke inside the circle radius
|
||||
const int stroke = 4;
|
||||
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
|
||||
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
|
||||
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
|
||||
|
||||
// accurate has no reach, so fall back to a visible marker size when drawing the circle
|
||||
int radius = touch_radius();
|
||||
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
|
||||
|
||||
// each slot holds a matured contact or the invalid sentinel; immature and absent
|
||||
// contacts were already filtered out by publish_touch_points
|
||||
for (auto &touch_point : TOUCH_POINTS) {
|
||||
POINT point = touch_point.load(std::memory_order_acquire);
|
||||
if (point.x == JB_INVALID_TOUCH_COORD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
|
||||
|
||||
SelectObject(hdc, pressed ? pen_white : pen_gray);
|
||||
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
|
||||
point.x + draw_radius, point.y + draw_radius);
|
||||
|
||||
// the arc points at a single snapped-to button; plus can trigger several at
|
||||
// once, so skip it there
|
||||
if (pressed || TOUCH_ALGORITHM == Plus) {
|
||||
continue;
|
||||
}
|
||||
int index = nearest_button(point.x, point.y, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
SelectObject(hdc, pen_white);
|
||||
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SelectObject(hdc, old_pen);
|
||||
DeleteObject(pen_white);
|
||||
DeleteObject(pen_gray);
|
||||
}
|
||||
|
||||
void touch_draw_debug_overlay(HDC hdc) {
|
||||
|
||||
// only draw while touch is active
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool show_boxes = debug_show_boxes();
|
||||
bool show_taps = debug_show_taps();
|
||||
if (!show_boxes && !show_taps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// legacy divides the field evenly; the other algorithms use button squares
|
||||
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
||||
AxisGeometry gx {}, gy {};
|
||||
if (!legacy) {
|
||||
touch_geometry(gx, gy);
|
||||
}
|
||||
|
||||
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
|
||||
if (show_boxes) {
|
||||
draw_debug_boxes(hdc, legacy, gx, gy);
|
||||
}
|
||||
if (show_taps) {
|
||||
draw_debug_taps(hdc, legacy, gx, gy);
|
||||
}
|
||||
SelectObject(hdc, old_brush);
|
||||
}
|
||||
|
||||
void touch_attach() {
|
||||
clear_touch_points();
|
||||
TOUCH_ENABLE.store(true, std::memory_order_release);
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case Plus:
|
||||
log_info("jubeat", "using 'plus' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo, this is a bug");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void touch_detach() {
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch detection algorithm
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
Plus,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// debug touch overlay mode
|
||||
enum JubeatTouchDebugMode {
|
||||
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
|
||||
JbTouchDebugNone, // draw nothing
|
||||
JbTouchDebugBox, // boundary boxes
|
||||
JbTouchDebugAll, // boundary boxes and touch circles
|
||||
};
|
||||
|
||||
// selected algorithm and debug mode (set from the launcher options)
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
|
||||
|
||||
// minimum contact age in milliseconds before a touch registers (0 = off);
|
||||
// filters momentary phantom touches from noisy panels
|
||||
extern uint32_t TOUCH_DEBOUNCE_MS;
|
||||
|
||||
// atomically published panel state, read by the I/O layer
|
||||
std::bitset<16> touch_state();
|
||||
|
||||
// read the current touch points and refresh the panel state
|
||||
void touch_update();
|
||||
|
||||
// enable/disable touch handling on game attach/detach
|
||||
void touch_attach();
|
||||
void touch_detach();
|
||||
|
||||
// whether the debug overlay should currently draw anything
|
||||
bool touch_debug_overlay_enabled();
|
||||
|
||||
// draw the debug touch overlay (boundary boxes and/or touch circles per the
|
||||
// selected mode) onto the spice touch overlay window's device context
|
||||
void touch_draw_debug_overlay(HDC hdc);
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2a_hook.h"
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "nost.h"
|
||||
#include "poke.h"
|
||||
#include "touch_mode.h"
|
||||
#include "hooks/setupapihook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
namespace games::nost {
|
||||
|
||||
bool ENABLE_POKE = false;
|
||||
bool ENABLE_TOUCH_MODE = false;
|
||||
|
||||
NostGame::NostGame() : Game("Nostalgia") {
|
||||
}
|
||||
@@ -43,9 +42,16 @@ namespace games::nost {
|
||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||
setupapihook_add(touch_settings);
|
||||
|
||||
// custom touch
|
||||
// nostalgia crashes if you inject touch events too early
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
} else {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NostGame::post_attach() {
|
||||
@@ -55,6 +61,9 @@ namespace games::nost {
|
||||
}
|
||||
|
||||
void NostGame::detach() {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::disable();
|
||||
}
|
||||
if (ENABLE_POKE) {
|
||||
poke::disable();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace games::nost {
|
||||
|
||||
extern bool ENABLE_POKE;
|
||||
extern bool ENABLE_TOUCH_MODE;
|
||||
|
||||
class NostGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "games/nost/io.h"
|
||||
#include "cfg/api.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
namespace games::nost::poke {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
static volatile bool THREAD_RUNNING = false;
|
||||
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
||||
static std::atomic<bool> THREAD_RUNNING { false };
|
||||
|
||||
// positions use game-client pixels, matching the legacy client-sized touch window
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
|
||||
{EAM_IO_KEYPAD_0, {760, 440}},
|
||||
{EAM_IO_KEYPAD_1, {760, 385}},
|
||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||
@@ -31,7 +34,8 @@ namespace games::nost::poke {
|
||||
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
||||
};
|
||||
|
||||
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
|
||||
static const std::unordered_map<
|
||||
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
|
||||
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
||||
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||
@@ -46,13 +50,8 @@ namespace games::nost::poke {
|
||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||
};
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
static bool inject_poke_position(POINT position, bool down) {
|
||||
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||
}
|
||||
|
||||
void enable() {
|
||||
@@ -61,126 +60,143 @@ namespace games::nost::poke {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nativetouch::is_hooked()) {
|
||||
log_warning("poke", "Nostalgia poke requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||
|
||||
const int swipe_anim_total_frames = 6;
|
||||
const int swipe_anim_y = 300;
|
||||
|
||||
const int swipe_next_page_x_begin = 1120;
|
||||
const int swipe_next_page_x_end = 1120-120;
|
||||
const int swipe_next_page_x_end = 1120 - 120;
|
||||
|
||||
const int swipe_prev_page_x_begin = 280-120;
|
||||
const int swipe_prev_page_x_begin = 280 - 120;
|
||||
const int swipe_prev_page_x_end = 280;
|
||||
|
||||
int next_page_anim_index = -1;
|
||||
int prev_page_anim_index = -1;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
|
||||
bool touch_release = false;
|
||||
bool contact_active = false;
|
||||
bool release_pending = false;
|
||||
POINT last_position {};
|
||||
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||
uint16_t last_keypad_state = 0;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
// clean up touch from last frame
|
||||
if (!touch_points.empty()) {
|
||||
if (touch_release) {
|
||||
clear_touch_points(&touch_points);
|
||||
touch_release = false;
|
||||
} else {
|
||||
touch_points.clear();
|
||||
if (release_pending) {
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
contact_active = false;
|
||||
release_pending = false;
|
||||
}
|
||||
|
||||
auto &buttons = games::nost::get_buttons();
|
||||
std::optional<POINT> touch_position;
|
||||
bool release_after_touch = false;
|
||||
|
||||
const auto button_triggered = [&](games::nost::Buttons::Button button) {
|
||||
const auto pressed =
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
|
||||
const auto triggered = pressed && !button_states[button];
|
||||
button_states[button] = pressed;
|
||||
return triggered;
|
||||
};
|
||||
|
||||
std::optional<POINT> triggered_touch_position;
|
||||
for (const auto& it : NOST_POKE_BUTTONS) {
|
||||
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const auto next_triggered =
|
||||
button_triggered(games::nost::Buttons::PokeNextPage);
|
||||
const auto prev_triggered =
|
||||
button_triggered(games::nost::Buttons::PokePrevPage);
|
||||
|
||||
const auto keypad_state = eamuse_get_keypad_state(0);
|
||||
if (!triggered_touch_position.has_value()) {
|
||||
for (const auto& it : NOST_POKE_KEYPAD) {
|
||||
const auto mask = static_cast<uint16_t>(1 << it.first);
|
||||
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_keypad_state = keypad_state;
|
||||
|
||||
if (0 <= next_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_next_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
next_page_anim_index--;
|
||||
if (next_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else if (0 <= prev_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_prev_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
prev_page_anim_index--;
|
||||
if (prev_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& it : NOST_POKE) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!touch_release) {
|
||||
const auto state = eamuse_get_keypad_state(0);
|
||||
if (state) {
|
||||
for (const auto& it : NOST_POKE_NUM) {
|
||||
if (state & (1 << it.first)) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
touch_position = triggered_touch_position;
|
||||
release_after_touch = touch_position.has_value();
|
||||
|
||||
// start animations for next frame
|
||||
if (!touch_release) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
||||
if (!touch_position.has_value()) {
|
||||
if (next_triggered) {
|
||||
next_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
|
||||
|
||||
if (prev_triggered) {
|
||||
prev_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
touch_write_points(&touch_points);
|
||||
if (touch_position.has_value() &&
|
||||
inject_poke_position(*touch_position, true)) {
|
||||
contact_active = true;
|
||||
last_position = *touch_position;
|
||||
}
|
||||
if (release_after_touch && contact_active) {
|
||||
release_pending = true;
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(30);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
}
|
||||
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "touch_mode.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// native contact positions feed piano input directly. native events reach the game
|
||||
// in nav mode and are suppressed in piano mode. mode-button contacts are always
|
||||
// suppressed and change that routing after release.
|
||||
static constexpr LONG PIANO_LEFT_GAP = 11;
|
||||
static constexpr LONG PIANO_RIGHT_GAP = 10;
|
||||
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||
|
||||
struct TouchGeometry {
|
||||
HWND window = nullptr;
|
||||
RECT mode_button {};
|
||||
LONG client_width = 0;
|
||||
LONG client_height = 0;
|
||||
|
||||
bool valid() const {
|
||||
return window != nullptr && client_width > 0 && client_height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct NativeContact {
|
||||
POINT position {};
|
||||
|
||||
// position contains game-client coordinates
|
||||
bool client_position_valid = false;
|
||||
|
||||
// down began on the mode switch button; remains true through up
|
||||
bool mode_button = false;
|
||||
};
|
||||
|
||||
static std::atomic_bool accept_events { false };
|
||||
static std::atomic<Mode> current_mode_state { Mode::Nav };
|
||||
static std::mutex state_mutex;
|
||||
static TouchGeometry touch_geometry;
|
||||
|
||||
// native contacts are kept by ID so each contact contributes exactly one position
|
||||
static std::unordered_map<DWORD, NativeContact> active_contacts;
|
||||
|
||||
// a hardware button release requests one change after all contacts are released
|
||||
static bool mode_change_pending = false;
|
||||
|
||||
static void reset_state_locked() {
|
||||
current_mode_state.store(Mode::Nav, std::memory_order_release);
|
||||
touch_geometry = {};
|
||||
active_contacts.clear();
|
||||
mode_change_pending = false;
|
||||
}
|
||||
|
||||
// hardware contacts arrive in screen coordinates
|
||||
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
|
||||
if (!touch_geometry.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT position { event.x, event.y };
|
||||
if (!ScreenToClient(touch_geometry.window, &position)) {
|
||||
return false;
|
||||
}
|
||||
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
|
||||
// first, process down / move events
|
||||
if (event.down || event.move) {
|
||||
auto contact = active_contacts.try_emplace(event.id).first;
|
||||
|
||||
// keep track of IDs that began as a down on the mode switch button
|
||||
if (event.down) {
|
||||
contact->second.mode_button = native_touch_in_button(event);
|
||||
}
|
||||
|
||||
// check for valid position
|
||||
POINT position { event.x, event.y };
|
||||
if (touch_geometry.window != nullptr &&
|
||||
ScreenToClient(touch_geometry.window, &position)) {
|
||||
contact->second.position = position;
|
||||
contact->second.client_position_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto contact = active_contacts.find(event.id);
|
||||
const bool mode_button_contact = contact != active_contacts.end() &&
|
||||
contact->second.mode_button;
|
||||
|
||||
// process up events
|
||||
if (event.up) {
|
||||
active_contacts.erase(event.id);
|
||||
|
||||
// if a contact that began down event on the mode switch button has
|
||||
// been released, a mode switch is now pending
|
||||
if (mode_button_contact) {
|
||||
mode_change_pending = true;
|
||||
}
|
||||
|
||||
// apply the change on the final hardware up. switching earlier would
|
||||
// split another contact's down and up events across different modes
|
||||
if (mode_change_pending && active_contacts.empty()) {
|
||||
mode_change_pending = false;
|
||||
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
|
||||
current_mode_state.store(next_mode, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
return mode_button_contact;
|
||||
}
|
||||
|
||||
// install the Nostalgia-specific native touch interception
|
||||
void enable() {
|
||||
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(filter_native_touch);
|
||||
log_info("nost::touch", "enabled");
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(nullptr);
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
bool enabled() {
|
||||
return accept_events.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
Mode current_mode() {
|
||||
return current_mode_state.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// publish the rendered overlay button rectangle in game-client pixels
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds) {
|
||||
TouchGeometry next {};
|
||||
RECT client_rect {};
|
||||
if (window != nullptr && GetClientRect(window, &client_rect) &&
|
||||
client_rect.right > 0 && client_rect.bottom > 0) {
|
||||
next.window = window;
|
||||
next.mode_button = client_bounds;
|
||||
next.client_width = client_rect.right;
|
||||
next.client_height = client_rect.bottom;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
touch_geometry = next;
|
||||
}
|
||||
|
||||
// return the active 28-key piano bitfield for the PANB input update
|
||||
uint32_t piano_key_state() {
|
||||
if (!enabled() || current_mode() != Mode::Piano) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t state = 0;
|
||||
for (const auto &contact : active_contacts) {
|
||||
// invalid position or mode-button contact; ignore these contacts
|
||||
if (!contact.second.client_position_valid || contact.second.mode_button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &position = contact.second.position;
|
||||
|
||||
// outside the client area or on the mode button; ignore these contacts
|
||||
if (position.x < 0 || position.x >= touch_geometry.client_width ||
|
||||
position.y < 0 || position.y >= touch_geometry.client_height ||
|
||||
PtInRect(&touch_geometry.mode_button, position)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// divide the inset width evenly; touches in either side gap clamp to
|
||||
// the nearest outer key so the physical screen edges remain playable
|
||||
const auto piano_width =
|
||||
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
|
||||
uint32_t key = 0;
|
||||
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
|
||||
key = PIANO_KEY_COUNT - 1;
|
||||
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
|
||||
key = static_cast<uint32_t>(
|
||||
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
|
||||
}
|
||||
state |= UINT32_C(1) << key;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// update native contacts and report whether this event should be hidden from the game
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
|
||||
// synthetic events are outside this hardware-only feature
|
||||
if (!enabled() || event.synthetic) {
|
||||
// false leaves the event visible to the game
|
||||
return false;
|
||||
}
|
||||
|
||||
// snapshot routing before an up event can commit a pending mode switch
|
||||
const bool piano_mode_before_update = current_mode() == Mode::Piano;
|
||||
|
||||
// update the contact lifetime and commit any pending switch when safe
|
||||
const bool mode_button_contact = update_touch_state(event);
|
||||
|
||||
// hide every event in a contact that began on the mode switch button
|
||||
if (mode_button_contact) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// piano mode consumes hardware events; nav mode forwards them to the game
|
||||
return piano_mode_before_update;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// nav mode forwards contacts to the game; piano mode converts them into piano keys
|
||||
enum class Mode {
|
||||
Nav,
|
||||
Piano,
|
||||
};
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool enabled();
|
||||
|
||||
Mode current_mode();
|
||||
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds);
|
||||
|
||||
uint32_t piano_key_state();
|
||||
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "util/unity_player.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "io.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "io.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::popn {
|
||||
|
||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
@@ -715,13 +715,16 @@ namespace games::popn {
|
||||
// 00000100 0000000B 00000001 (button 9)
|
||||
// set third column to 0 and it will work with BIO2
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
wintouchemu::FORCE = true;
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
||||
wintouchemu::hook_title_ends(
|
||||
"",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
||||
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
|
||||
}
|
||||
|
||||
// init stuff
|
||||
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
|
||||
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
log_info("rb", "not changing touch poll rate (120Hz by default)");
|
||||
log_info("rb", "not changing touch poll rate (~125Hz by default)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
namespace games::rb {
|
||||
|
||||
extern uint16_t TOUCH_SCALING;
|
||||
extern uint8_t TOUCH_SIZE;
|
||||
extern uint16_t TOUCH_POLL_RATE;
|
||||
|
||||
class RBGame : public games::Game {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch_debug.h"
|
||||
#include "touch_defs.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
@@ -12,28 +15,38 @@
|
||||
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
||||
|
||||
namespace games::rb {
|
||||
uint16_t TOUCH_SCALING = 1000;
|
||||
uint8_t TOUCH_SIZE = 1;
|
||||
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
|
||||
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
|
||||
|
||||
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
|
||||
|
||||
static void packet_set_bit(unsigned char *packet, int bit) {
|
||||
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
|
||||
static_cast<unsigned char>(1u << (bit % 8));
|
||||
}
|
||||
}
|
||||
|
||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
||||
this->log_fps = log_fps;
|
||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
|
||||
}
|
||||
|
||||
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
||||
|
||||
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
|
||||
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
|
||||
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
|
||||
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
|
||||
// read() contracts X uniformly so screen edges land within usable sensors 2..45
|
||||
int grid_x = CLAMP(
|
||||
(cursor_x * X_SENSOR_COUNT) / window_width,
|
||||
0,
|
||||
X_SENSOR_COUNT - 1);
|
||||
int grid_y = CLAMP(
|
||||
(cursor_y * Y_SENSOR_COUNT) / window_height,
|
||||
0,
|
||||
Y_SENSOR_COUNT - 1);
|
||||
|
||||
// get bit positions
|
||||
int bit_x = 88 + grid_x;
|
||||
int bit_y = 74 - grid_y;
|
||||
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
|
||||
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
|
||||
|
||||
// insert bits
|
||||
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
|
||||
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
|
||||
packet_set_bit(data, bit_x);
|
||||
packet_set_bit(data, bit_y);
|
||||
}
|
||||
|
||||
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
|
||||
if (wnd != nullptr) {
|
||||
|
||||
// cache the game window so read() can size against it regardless of focus
|
||||
this->game_hwnd = wnd;
|
||||
|
||||
// reset window process to make the game not crash
|
||||
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
||||
|
||||
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
// update window
|
||||
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} else {
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
// show game window because it lost focus
|
||||
// show fullscreen game window because it lost focus
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
ShowWindow(wnd, SW_SHOW);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// fallback to dx hook
|
||||
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
touch_debug_attach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||
|
||||
// check buffer size
|
||||
if (nNumberOfBytesToRead < 20) {
|
||||
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get window
|
||||
HWND window = GetForegroundWindow();
|
||||
// size against the game window cached in open() (not GetForegroundWindow), so
|
||||
// touch stays correctly scaled and keeps working when the game loses focus
|
||||
HWND window = this->game_hwnd;
|
||||
if (window == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// create data
|
||||
unsigned char data[20] {};
|
||||
unsigned char data[TOUCH_PACKET_SIZE] {};
|
||||
|
||||
// data header
|
||||
data[0] = 0x55;
|
||||
data[2] = 0x4C;
|
||||
|
||||
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
|
||||
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
|
||||
|
||||
// iterate all touch points
|
||||
auto offset_x = (int) (window_width / 48.0 / 3.0);
|
||||
auto offset_y = (int) (window_height / 76.0 / 3.0);
|
||||
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||
std::vector<TouchPoint> touch_points;
|
||||
touch_get_points(touch_points);
|
||||
for (auto &point : touch_points) {
|
||||
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// apply scaling
|
||||
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
|
||||
x = x / SCALE_FACTOR;
|
||||
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
|
||||
y = y / SCALE_FACTOR;
|
||||
x = x - (window_width * (1.f - scale_factor) / 2);
|
||||
x = x / scale_factor;
|
||||
y = y - (window_height * (1.f - scale_factor) / 2);
|
||||
y = y / scale_factor;
|
||||
|
||||
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// point scaling
|
||||
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
||||
const auto point_y = (int) (y - window_height / 76);
|
||||
// point coordinates
|
||||
// keep the verified left edge fixed while adding up to one-third of a beam
|
||||
// at the right edge, preventing the finger footprint from including sensor 44
|
||||
const auto point_x = (int) (
|
||||
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
|
||||
window_width / 2.0 +
|
||||
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
|
||||
const auto point_y = (int) y;
|
||||
|
||||
// center
|
||||
grid_insert(data, point_x, point_y);
|
||||
|
||||
// surrounding points
|
||||
if (games::rb::TOUCH_SIZE == 3) {
|
||||
grid_insert(data, point_x - offset_x, point_y); // west
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||
grid_insert(data, point_x + offset_x, point_y); // east
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
|
||||
grid_insert(data, point_x, point_y - offset_y); // north
|
||||
grid_insert(data, point_x, point_y + offset_y); // south
|
||||
}
|
||||
// model a finger as a 3x3 block of IR sensors around the touch point
|
||||
// this gives better accuracy (than just 1x1) since the logic below
|
||||
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
|
||||
// the center point, which means by inserting up to 8 extra blocks
|
||||
// we are emulating a sub-"pixel" resolution
|
||||
grid_insert(data, point_x, point_y); // center
|
||||
grid_insert(data, point_x - offset_x, point_y); // west
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||
grid_insert(data, point_x + offset_x, point_y); // east
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
|
||||
grid_insert(data, point_x, point_y - offset_y); // north
|
||||
grid_insert(data, point_x, point_y + offset_y); // south
|
||||
}
|
||||
|
||||
touch_debug_publish(data, is_landscape);
|
||||
|
||||
// copy data to buffer
|
||||
memcpy(lpBuffer, data, 20);
|
||||
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
|
||||
|
||||
// update frame logging
|
||||
if (log_fps) {
|
||||
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
}
|
||||
|
||||
// return amount of bytes written
|
||||
return 20;
|
||||
return TOUCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
||||
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
|
||||
|
||||
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
||||
|
||||
touch_debug_detach();
|
||||
|
||||
// detach touch module
|
||||
touch_detach();
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace games::rb {
|
||||
int window_width = 1080;
|
||||
int window_height = 1920;
|
||||
|
||||
// game window resolved in open(); read() sizes against this instead of the
|
||||
// foreground window so touch stays correctly scaled and keeps working when
|
||||
// the game is not the foreground window (rawinput delivers touch regardless
|
||||
// of focus)
|
||||
HWND game_hwnd = nullptr;
|
||||
|
||||
// logging
|
||||
bool log_fps = false;
|
||||
uint64_t log_time = 0;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "touch_debug.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "games/rb/rb.h"
|
||||
#include "games/rb/touch_defs.h"
|
||||
|
||||
namespace games::rb {
|
||||
|
||||
struct TouchDebugState {
|
||||
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
|
||||
bool is_landscape = false;
|
||||
};
|
||||
|
||||
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
|
||||
static std::atomic_bool TOUCH_ACTIVE = false;
|
||||
static std::mutex TOUCH_DEBUG_STATE_M;
|
||||
static TouchDebugState TOUCH_DEBUG_STATE;
|
||||
|
||||
static float touch_scale_factor() {
|
||||
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
|
||||
}
|
||||
|
||||
static void clear_touch_debug_state() {
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
TOUCH_DEBUG_STATE = {};
|
||||
}
|
||||
|
||||
static TouchDebugState get_touch_debug_state() {
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
return TOUCH_DEBUG_STATE;
|
||||
}
|
||||
|
||||
static bool packet_bit_active(
|
||||
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
|
||||
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
|
||||
}
|
||||
|
||||
static int sensor_center(int sensor, int sensor_count, int extent) {
|
||||
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
|
||||
}
|
||||
|
||||
static float sensor_span_position(int sensor, int sensor_count, int extent) {
|
||||
return sensor * (extent - 1) / (float) (sensor_count - 1);
|
||||
}
|
||||
|
||||
bool touch_debug_overlay_enabled() {
|
||||
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void touch_draw_debug_overlay() {
|
||||
if (!touch_debug_overlay_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &io = ImGui::GetIO();
|
||||
int width = static_cast<int>(io.DisplaySize.x);
|
||||
int height = static_cast<int>(io.DisplaySize.y);
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float scale_factor = touch_scale_factor();
|
||||
const float left = width * (1.f - scale_factor) / 2.f;
|
||||
const float top = height * (1.f - scale_factor) / 2.f;
|
||||
const float right = width - left;
|
||||
const float bottom = height - top;
|
||||
|
||||
TouchDebugState state = get_touch_debug_state();
|
||||
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
|
||||
auto draw_line = [&](float x1, float y1, float x2, float y2) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(x1, y1), ImVec2(x2, y2),
|
||||
IM_COL32(0, 255, 64, 255), 2.f);
|
||||
};
|
||||
|
||||
// show the valid input area when touch scaling restricts it
|
||||
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
|
||||
draw_list->AddRect(
|
||||
ImVec2(left, top), ImVec2(right, bottom),
|
||||
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
|
||||
}
|
||||
|
||||
// spread the usable X sensors 2..45 from edge to edge
|
||||
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
|
||||
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float position = sensor_span_position(
|
||||
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
|
||||
state.is_landscape ? height : width);
|
||||
if (state.is_landscape) {
|
||||
float y = top + position * scale_factor;
|
||||
draw_line(left, y, right, y);
|
||||
} else {
|
||||
float x = left + position * scale_factor;
|
||||
draw_line(x, top, x, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
|
||||
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int position = sensor_center(
|
||||
sensor, Y_SENSOR_COUNT,
|
||||
state.is_landscape ? width : height);
|
||||
if (state.is_landscape) {
|
||||
float x = right - position * scale_factor;
|
||||
draw_line(x, top, x, bottom);
|
||||
} else {
|
||||
float y = top + position * scale_factor;
|
||||
draw_line(left, y, right, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void touch_debug_attach() {
|
||||
clear_touch_debug_state();
|
||||
TOUCH_ACTIVE.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void touch_debug_detach() {
|
||||
TOUCH_ACTIVE.store(false, std::memory_order_release);
|
||||
clear_touch_debug_state();
|
||||
}
|
||||
|
||||
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
|
||||
if (!TOUCH_DEBUG_OVERLAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
|
||||
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace games::rb {
|
||||
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
|
||||
|
||||
bool touch_debug_overlay_enabled();
|
||||
void touch_draw_debug_overlay();
|
||||
|
||||
void touch_debug_attach();
|
||||
void touch_debug_detach();
|
||||
void touch_debug_publish(const unsigned char *data, bool is_landscape);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::rb {
|
||||
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
|
||||
inline constexpr int TOUCH_PACKET_SIZE = 20;
|
||||
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
|
||||
|
||||
inline constexpr int X_SENSOR_COUNT = 48;
|
||||
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
|
||||
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
|
||||
inline constexpr int X_SENSOR_ACTIVE_COUNT =
|
||||
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
|
||||
inline constexpr int X_SENSOR_FIRST_BIT = 88;
|
||||
|
||||
inline constexpr int Y_SENSOR_COUNT = 76;
|
||||
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#ifdef SPICE64
|
||||
#include "hooks/graphics/nvapi_impl.h"
|
||||
#endif
|
||||
#include "hooks/graphics/nvapi_hook.h"
|
||||
#include "hooks/powrprof.h"
|
||||
#include "hooks/sleephook.h"
|
||||
@@ -23,8 +26,8 @@
|
||||
#include "util/libutils.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "camera.h"
|
||||
#include "io.h"
|
||||
@@ -46,7 +49,6 @@ namespace games::sdvx {
|
||||
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
||||
|
||||
// settings
|
||||
bool NATIVETOUCH = false;
|
||||
uint8_t DIGITAL_KNOB_SENS = 16;
|
||||
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
||||
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
||||
@@ -428,28 +430,41 @@ namespace games::sdvx {
|
||||
if (aio != nullptr) {
|
||||
|
||||
// enable 9on12 for AMD
|
||||
if (!libutils::try_library("nvapi64.dll")) {
|
||||
const auto nvapi = libutils::try_library("nvapi64.dll");
|
||||
if (nvapi == nullptr) {
|
||||
log_info(
|
||||
"sdvx",
|
||||
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
||||
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
|
||||
}
|
||||
|
||||
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
|
||||
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
|
||||
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
|
||||
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
|
||||
if (!nvapi_impl::initialize(
|
||||
avs::game::DLL_INSTANCE,
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz)) {
|
||||
log_warning("sdvx", "failed to initialize synthetic NVAPI");
|
||||
}
|
||||
} else {
|
||||
// don't let nvapi mess with display settings
|
||||
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
if (is_valkyrie_model()) {
|
||||
if (NATIVETOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
||||
// hook touch window
|
||||
// in windowed mode, game can accept mouse input on the second screen
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
"SOUND VOLTEX",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// insert BI2X hooks
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace games::sdvx {
|
||||
};
|
||||
|
||||
// settings
|
||||
extern bool NATIVETOUCH;
|
||||
extern uint8_t DIGITAL_KNOB_SENS;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern SdvxOverlayPosition OVERLAY_POS;
|
||||
|
||||
@@ -32,18 +32,28 @@ namespace hooks::audio {
|
||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||
index++) {
|
||||
|
||||
// read description (display name), fall back to the key name
|
||||
// read description (display name), fall back to the key name.
|
||||
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||
// because the latter is unavailable on Windows XP.
|
||||
char desc[256] = { 0 };
|
||||
DWORD size = sizeof(desc);
|
||||
std::string name = key_name;
|
||||
if (RegGetValueA(hkEnum,
|
||||
key_name,
|
||||
ASIO_REG_DESC,
|
||||
RRF_RT_REG_SZ | wow64_flag,
|
||||
nullptr,
|
||||
desc,
|
||||
&size) == ERROR_SUCCESS && desc[0]) {
|
||||
name = desc;
|
||||
HKEY hkDriver = nullptr;
|
||||
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||
DWORD type = 0;
|
||||
if (RegQueryValueExA(hkDriver,
|
||||
ASIO_REG_DESC,
|
||||
nullptr,
|
||||
&type,
|
||||
reinterpret_cast<LPBYTE>(desc),
|
||||
&size) == ERROR_SUCCESS
|
||||
&& type == REG_SZ && desc[0]) {
|
||||
// ensure null termination
|
||||
desc[sizeof(desc) - 1] = '\0';
|
||||
name = desc;
|
||||
}
|
||||
RegCloseKey(hkDriver);
|
||||
}
|
||||
|
||||
// merge with an existing entry from the other view (match by name)
|
||||
|
||||
@@ -154,36 +154,30 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
||||
// stale one down
|
||||
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
|
||||
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
|
||||
// is destroyed and re-created within a process - which the host triggers by leaking and
|
||||
// re-instantiating the driver during startup probing. so we build the real driver and
|
||||
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
|
||||
// the same wrapper back for every later CoCreate. as a result the real driver is created
|
||||
// and initialized exactly once
|
||||
std::mutex g_wrappers_mutex;
|
||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||
|
||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
||||
// replaces so the caller can tear it down outside the lock
|
||||
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||
WrappedAsio *find_wrapper(REFCLSID clsid) {
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
for (auto &entry : g_wrappers) {
|
||||
if (IsEqualCLSID(entry.first, clsid)) {
|
||||
WrappedAsio *stale = entry.second;
|
||||
entry.second = wrapper;
|
||||
return stale;
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
|
||||
g_wrappers.emplace_back(clsid, wrapper);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
||||
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
||||
if (it->second == wrapper) {
|
||||
g_wrappers.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_wrappers.emplace_back(clsid, wrapper);
|
||||
}
|
||||
|
||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
|
||||
}
|
||||
|
||||
WrappedAsio::~WrappedAsio() {
|
||||
unregister_wrapper(this);
|
||||
|
||||
this->detach_post_process();
|
||||
|
||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
||||
// exactly one here and this releases/unloads it deterministically
|
||||
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
|
||||
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
|
||||
// only once the host has released its own references. tears down the real driver
|
||||
this->pReal->Release();
|
||||
|
||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||
// the real driver is single-instance and kept alive; initialize it exactly once. the
|
||||
// host re-calls init() on each CoCreate during probing, but re-initializing a live
|
||||
// driver crashes some hardware drivers, so once we have a live driver we report success
|
||||
if (this->initialized) {
|
||||
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
|
||||
return AsioTrue;
|
||||
}
|
||||
|
||||
const AsioBool result = this->pReal->init(sys_handle);
|
||||
if (result == AsioTrue) {
|
||||
this->initialized = true;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"init succeeded for '{}' (driver version {})",
|
||||
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
|
||||
AsioError __thiscall WrappedAsio::start() {
|
||||
const AsioError result = this->pReal->start();
|
||||
if (result == ASE_OK) {
|
||||
this->started = true;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"start succeeded, ASIO stream is now running on '{}'",
|
||||
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
|
||||
AsioError __thiscall WrappedAsio::stop() {
|
||||
const AsioError result = this->pReal->stop();
|
||||
if (result == ASE_OK) {
|
||||
this->started = false;
|
||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
|
||||
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
||||
}
|
||||
|
||||
void WrappedAsio::quiesce_for_reuse() {
|
||||
// a previous abandoned probing cycle may have left a running stream and live buffers on
|
||||
// the real driver without calling stop/dispose; tear that down now (without destroying
|
||||
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
|
||||
// further buffer_switch, and dispose_buffers detaches our trampolines
|
||||
if (this->started) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"reuse: stopping leftover stream on '{}' before handing the driver back",
|
||||
this->driver_name);
|
||||
this->stop();
|
||||
}
|
||||
if (this->buffers_created) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"reuse: disposing leftover buffers on '{}' before handing the driver back",
|
||||
this->driver_name);
|
||||
this->dispose_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||
return;
|
||||
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
|
||||
}
|
||||
}
|
||||
this->publish_post_process(buffer_size);
|
||||
this->buffers_created = true;
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
|
||||
// device's 2.0 output. then publish ourselves to the realtime thread
|
||||
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
||||
this->publish_post_process(buffer_size);
|
||||
this->buffers_created = true;
|
||||
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
|
||||
this->volume_channels.clear();
|
||||
this->volume_active = false;
|
||||
this->downmix_active = false;
|
||||
this->buffers_created = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
|
||||
auto *wrapper = new WrappedAsio(
|
||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||
|
||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
||||
// deref once before re-initializing.
|
||||
//
|
||||
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
||||
// Xonar AE can
|
||||
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"host did not release previous instance for clsid={}, forcing teardown",
|
||||
guid2s(clsid));
|
||||
|
||||
// the stale instance may still have a running stream on the driver's realtime
|
||||
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
||||
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
||||
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
||||
// detaches our trampolines, fully quiescing the realtime path before delete
|
||||
stale->stop();
|
||||
stale->dispose_buffers();
|
||||
delete stale;
|
||||
}
|
||||
// pin the wrapper (and the real driver it owns) for the process lifetime; later
|
||||
// CoCreate calls reuse this same instance via wrap_existing
|
||||
wrapper->AddRef();
|
||||
store_wrapper(clsid, wrapper);
|
||||
|
||||
return static_cast<IAsio *>(wrapper);
|
||||
}
|
||||
|
||||
IUnknown *wrap_existing(REFCLSID clsid) {
|
||||
WrappedAsio *wrapper = find_wrapper(clsid);
|
||||
if (wrapper == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"audio::wrappedasio",
|
||||
"reusing cached ASIO driver instance, clsid={}",
|
||||
guid2s(clsid));
|
||||
|
||||
wrapper->quiesce_for_reuse();
|
||||
|
||||
// hand the host another reference to the one instance we keep alive
|
||||
wrapper->AddRef();
|
||||
return static_cast<IAsio *>(wrapper);
|
||||
}
|
||||
|
||||
void release_all_wrappers() {
|
||||
// collect the pinned wrappers under the lock, then drop our references outside it so
|
||||
// a final Release (which can run the driver's own teardown) never happens while the
|
||||
// lock is held
|
||||
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
|
||||
{
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
wrappers = g_wrappers;
|
||||
g_wrappers.clear();
|
||||
}
|
||||
|
||||
for (auto &entry : wrappers) {
|
||||
// drop only the pin wrap() took. if the host has already released its own
|
||||
// references (the normal case at shutdown) this destroys the wrapper and releases
|
||||
// the real driver; if the host is still using it, the refcount stays above zero
|
||||
// and we leave its live stream/buffers untouched
|
||||
log_misc(
|
||||
"audio::wrappedasio",
|
||||
"releasing cached ASIO driver instance, clsid={}",
|
||||
guid2s(entry.first));
|
||||
entry.second->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||
|
||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||
// return a proxy that forwards every call to it
|
||||
// return a proxy that forwards every call to it. also records it as the cached
|
||||
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||
|
||||
// if a cached wrapper already exists for this CLSID, return it (with an added
|
||||
// reference); otherwise nullptr to signal the caller to create the real driver and
|
||||
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
||||
IUnknown *wrap_existing(REFCLSID clsid);
|
||||
|
||||
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
||||
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
||||
// has released its own references too. call from a controlled shutdown point, never from
|
||||
// a static destructor (the driver DLL may already be unloaded)
|
||||
void release_all_wrappers();
|
||||
}
|
||||
|
||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
|
||||
AsioError __thiscall output_ready() override;
|
||||
#pragma endregion
|
||||
|
||||
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
||||
// for reuse, without destroying the real driver (see wrap_existing)
|
||||
void quiesce_for_reuse();
|
||||
|
||||
private:
|
||||
// create_buffers implementation used when a stereo extraction is active: forwards only
|
||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||
@@ -170,6 +186,14 @@ private:
|
||||
// unambiguous name; constant for our lifetime
|
||||
std::string driver_name;
|
||||
|
||||
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
||||
bool initialized = false;
|
||||
|
||||
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
||||
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
||||
bool buffers_created = false;
|
||||
bool started = false;
|
||||
|
||||
// our own reference count; we hold one reference on pReal and release it when this
|
||||
// drops to zero
|
||||
std::atomic<ULONG> ref_count {1};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "audio_private.h"
|
||||
#include "acm.h"
|
||||
#include "asio_proxy.h"
|
||||
#include "xact.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
@@ -53,9 +55,39 @@ namespace hooks::audio {
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
// private globals
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
std::mutex INITIALIZE_LOCK; // for asio
|
||||
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
static std::mutex CLIENT_LOCK;
|
||||
static bool CLIENT_STOPPING = false;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source) {
|
||||
IAudioClient *previous_client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
|
||||
if (CLIENT_STOPPING || CLIENT == client) {
|
||||
return;
|
||||
}
|
||||
if (client) {
|
||||
client->AddRef();
|
||||
}
|
||||
|
||||
previous_client = CLIENT;
|
||||
CLIENT = client;
|
||||
}
|
||||
|
||||
if (previous_client) {
|
||||
previous_client->Release();
|
||||
}
|
||||
|
||||
if (client) {
|
||||
log_info(
|
||||
"audio",
|
||||
"active client selected by {} after successful initialization",
|
||||
source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
@@ -68,6 +100,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
|
||||
HRESULT ret;
|
||||
|
||||
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
|
||||
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
|
||||
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
|
||||
*ppv = reused;
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// call original
|
||||
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||
if (FAILED(ret)) {
|
||||
@@ -120,15 +161,28 @@ namespace hooks::audio {
|
||||
|
||||
// general hooks
|
||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||
if (avs::game::is_model("PAN")) {
|
||||
hooks::audio::xact::init();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
log_info("audio", "stopping");
|
||||
if (CLIENT) {
|
||||
CLIENT->Stop();
|
||||
CLIENT->Release();
|
||||
IAudioClient *client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
CLIENT_STOPPING = true;
|
||||
client = CLIENT;
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
if (client) {
|
||||
client->Stop();
|
||||
client->Release();
|
||||
}
|
||||
stop_low_latency();
|
||||
|
||||
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||
// at a controlled shutdown point (see asio_proxy.cpp)
|
||||
hooks::audio::asio::release_all_wrappers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ namespace hooks::audio {
|
||||
extern std::mutex INITIALIZE_LOCK;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source);
|
||||
}
|
||||
|
||||
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
}
|
||||
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
||||
|
||||
// release old audio client if initialized
|
||||
if (hooks::audio::CLIENT) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
IAudioClient *client = nullptr;
|
||||
if (iid == IID_IAudioClient) {
|
||||
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
||||
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
||||
}
|
||||
*ppInterface = client;
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||
|
||||
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// release any previously persisted client
|
||||
if (hooks::audio::CLIENT != nullptr) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "hooks/audio/util.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
NullDiscardBackend::~NullDiscardBackend() {
|
||||
this->running = false;
|
||||
@@ -125,8 +125,6 @@ HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||
void NullDiscardBackend::pace_loop() {
|
||||
using namespace std::chrono;
|
||||
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||
// game once per buffer period to keep its render thread from blocking on the event.
|
||||
const auto period = duration_cast<steady_clock::duration>(
|
||||
@@ -136,6 +134,6 @@ void NullDiscardBackend::pace_loop() {
|
||||
if (this->relay_handle) {
|
||||
SetEvent(this->relay_handle);
|
||||
}
|
||||
timer.sleep(period);
|
||||
std::this_thread::sleep_for(period);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||
}
|
||||
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
@@ -651,5 +652,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
HRESULT ret = this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
AudioSessionGuid);
|
||||
if (SUCCEEDED(ret)) {
|
||||
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
|
||||
}
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
#include "xact.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <mmreg.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
|
||||
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
|
||||
// layout, so keep this proxy pinned to the legacy ABI used by libxact.
|
||||
struct XAudio2DeviceDetails {
|
||||
WCHAR device_id[256];
|
||||
WCHAR display_name[256];
|
||||
DWORD role;
|
||||
WAVEFORMATEXTENSIBLE output_format;
|
||||
};
|
||||
|
||||
struct XAudio2EffectChain {
|
||||
UINT32 effect_count;
|
||||
const void *effect_descriptors;
|
||||
};
|
||||
|
||||
struct IXAudio2_27 {
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE Release() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
|
||||
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
|
||||
virtual void STDMETHODCALLTYPE StopEngine() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
|
||||
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
|
||||
virtual void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) = 0;
|
||||
};
|
||||
|
||||
// XAudio2 2.7 COM class and interface.
|
||||
DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
|
||||
0x5a508685, 0xa254, 0x4fba,
|
||||
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
|
||||
DEFINE_GUID(IID_IXAudio2_7_LEGACY,
|
||||
0x8bcf1f58, 0x9fe7, 0x4583,
|
||||
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
|
||||
|
||||
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
|
||||
|
||||
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
|
||||
static CreateFX_t CreateFX_orig = nullptr;
|
||||
|
||||
static std::string describe_wave_format(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
DWORD channel_mask = 0;
|
||||
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
|
||||
}
|
||||
|
||||
return fmt::format(
|
||||
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
|
||||
format->wFormatTag,
|
||||
format->nChannels,
|
||||
format->nSamplesPerSec,
|
||||
format->wBitsPerSample,
|
||||
format->nBlockAlign,
|
||||
format->nAvgBytesPerSec,
|
||||
channel_mask);
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
static std::string narrow_fixed(const WCHAR (&value)[Size]) {
|
||||
size_t length = 0;
|
||||
while (length < Size && value[length] != L'\0') {
|
||||
length++;
|
||||
}
|
||||
return ws2s(std::wstring(value, length));
|
||||
}
|
||||
|
||||
class WrappedXAudio2 final : public IXAudio2_27 {
|
||||
public:
|
||||
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
|
||||
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
|
||||
if (object == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = this;
|
||||
AddRef();
|
||||
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const auto result = real->QueryInterface(riid, object);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::QueryInterface({}) -> {}, object={}",
|
||||
guid2s(riid),
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++ref_count;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const auto remaining = --ref_count;
|
||||
if (remaining == 0) {
|
||||
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
|
||||
real->Release();
|
||||
delete this;
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
|
||||
const auto result = real->GetDeviceCount(device_count);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceCount -> {}, count={}",
|
||||
FMT_HRESULT(result),
|
||||
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) override {
|
||||
const auto result = real->GetDeviceDetails(device_index, device_details);
|
||||
if (SUCCEEDED(result) && device_details != nullptr) {
|
||||
const auto device_name = narrow_fixed(device_details->display_name);
|
||||
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result),
|
||||
narrow_fixed(device_details->device_id),
|
||||
device_name,
|
||||
device_details->role,
|
||||
describe_wave_format(&device_details->output_format.Format));
|
||||
}
|
||||
const auto channels = device_details->output_format.Format.nChannels;
|
||||
if (channels != 2 && channels != 6 &&
|
||||
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
|
||||
device_name,
|
||||
channels);
|
||||
deferredlogs::defer_error_messages({
|
||||
"unsupported audio output channel count detected!",
|
||||
fmt::format(" device: {}", device_name),
|
||||
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
|
||||
" * configure the default Windows playback device for stereo or 5.1 output",
|
||||
" * disable 7.1 surround sound or spatial audio for this device",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
|
||||
const auto result = real->Initialize(flags, processor);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
|
||||
flags,
|
||||
processor,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
|
||||
const auto result = real->RegisterForCallbacks(callback);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::RegisterForCallbacks({}) -> {}",
|
||||
callback,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
|
||||
real->UnregisterForCallbacks(callback);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSourceVoice(
|
||||
source_voice,
|
||||
source_format,
|
||||
flags,
|
||||
max_frequency_ratio,
|
||||
callback,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSubmixVoice(
|
||||
submix_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
processing_stage,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
const auto result = real->CreateMasteringVoice(
|
||||
mastering_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain != nullptr ? effect_chain->effect_count : 0,
|
||||
FMT_HRESULT(result),
|
||||
mastering_voice != nullptr ? *mastering_voice : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE StartEngine() override {
|
||||
const auto result = real->StartEngine();
|
||||
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE StopEngine() override {
|
||||
log_info("audio::xaudio2", "IXAudio2::StopEngine");
|
||||
real->StopEngine();
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
|
||||
return real->CommitChanges(operation_set);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
|
||||
real->GetPerformanceData(performance_data);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
|
||||
real->SetDebugConfiguration(debug_configuration, reserved);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_count = 1;
|
||||
std::atomic_bool device_details_logged = false;
|
||||
std::atomic_bool channel_warning_logged = false;
|
||||
IXAudio2_27 *real;
|
||||
};
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
REFCLSID clsid,
|
||||
LPUNKNOWN outer,
|
||||
DWORD class_context,
|
||||
REFIID iid,
|
||||
LPVOID *object) {
|
||||
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
|
||||
guid2s(clsid),
|
||||
guid2s(iid),
|
||||
class_context,
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
|
||||
if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
|
||||
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
|
||||
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = static_cast<IXAudio2_27 *>(
|
||||
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static HRESULT WINAPI CreateFX_hook(
|
||||
REFCLSID clsid,
|
||||
IUnknown **effect,
|
||||
const void *init_data,
|
||||
UINT32 init_data_size) {
|
||||
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
|
||||
log_info(
|
||||
"audio::xapofx",
|
||||
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
|
||||
guid2s(clsid),
|
||||
init_data,
|
||||
init_data_size,
|
||||
FMT_HRESULT(result),
|
||||
effect != nullptr ? static_cast<void *>(*effect) : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
void init() {
|
||||
const auto libxact = GetModuleHandleW(L"libxact.dll");
|
||||
if (libxact == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoCreateInstance_orig = detour::iat_try(
|
||||
"CoCreateInstance", CoCreateInstance_hook, libxact);
|
||||
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
|
||||
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"libxact hooks installed: CoCreateInstance={}, CreateFX={}",
|
||||
CoCreateInstance_orig != nullptr,
|
||||
CreateFX_orig != nullptr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
void init();
|
||||
}
|
||||
@@ -23,12 +23,13 @@
|
||||
#include "launcher/shutdown.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/touch_indicators.h"
|
||||
#include "touch/touch_gestures.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/utils.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "util/time.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
|
||||
@@ -188,11 +189,13 @@ static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
||||
}
|
||||
|
||||
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_override(window_name);
|
||||
}
|
||||
|
||||
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
||||
if (window_name == "LEFT") {
|
||||
if (window_name == "GITADORA") {
|
||||
GRAPHICS_HOOKED_WINDOW = hWnd;
|
||||
} else if (window_name == "LEFT") {
|
||||
GFDM_LEFT_WINDOW = hWnd;
|
||||
} else if (window_name == "RIGHT") {
|
||||
GFDM_RIGHT_WINDOW = hWnd;
|
||||
@@ -589,6 +592,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
hWndParent, hMenu, hInstance, lpParam);
|
||||
GRAPHICS_WINDOWS.push_back(result);
|
||||
|
||||
// remember these windows now because full capture happens later during D3D
|
||||
// initialization; placement calls before then can undo creation-time overrides
|
||||
if (result != nullptr && is_gfdm_window && !is_gfdm_sub_window) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
|
||||
// theme the native title bar (dark/light)
|
||||
set_window_dark_titlebar(result);
|
||||
|
||||
@@ -609,9 +618,6 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
}
|
||||
|
||||
// only hook touch window if multiple windows are allowed
|
||||
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||
@@ -627,7 +633,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
}
|
||||
}
|
||||
|
||||
disable_touch_indicators(result);
|
||||
disable_touch_gestures(result);
|
||||
log_misc(
|
||||
"graphics",
|
||||
"CreateWindowExA returned {}, {}",
|
||||
@@ -722,7 +728,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
|
||||
fmt::ptr(result),
|
||||
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
||||
|
||||
disable_touch_indicators(result);
|
||||
disable_touch_gestures(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -797,7 +803,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
||||
nWidth = rect.right - rect.left;
|
||||
nHeight = rect.bottom - rect.top;
|
||||
|
||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||
if (games::iidx::NATIVE_TOUCH) {
|
||||
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||
} else {
|
||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||
}
|
||||
} else {
|
||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||
log_info(
|
||||
@@ -950,6 +960,13 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (games::iidx::TDJ_MODE &&
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||
hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||
return true;
|
||||
}
|
||||
|
||||
// call original
|
||||
return ShowWindow_orig(hWnd, nCmdShow);
|
||||
}
|
||||
@@ -1163,6 +1180,14 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
||||
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||
|
||||
const bool native_touch_overlay =
|
||||
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||
(games::popn::NATIVE_TOUCH &&
|
||||
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||
if (native_touch_overlay) {
|
||||
nativetouch::inject::register_and_attach_window(hWnd);
|
||||
}
|
||||
|
||||
// NOLEGACY causes WM_CHAR to be not received
|
||||
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
||||
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
||||
|
||||
@@ -88,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
extern bool GRAPHICS_IIDX_WSUB;
|
||||
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
|
||||
@@ -157,7 +159,7 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change);
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name);
|
||||
|
||||
void change_primary_monitor(const std::string &monitor_name);
|
||||
void update_monitor_on_boot(
|
||||
|
||||
@@ -30,6 +30,8 @@ std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
|
||||
bool GRAPHICS_IIDX_WSUB = false;
|
||||
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
|
||||
return empty;
|
||||
}
|
||||
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name) {
|
||||
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
||||
return !monitor_name.empty();
|
||||
return !monitor_name.empty() ||
|
||||
(window_name == "SMALL" &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() ||
|
||||
GRAPHICS_GITADORA_SMALL_POS.has_value()));
|
||||
}
|
||||
|
||||
static bool graphics_monitor_rect_from_name(
|
||||
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change) {
|
||||
return graphics_gitadora_apply_monitor_rect(
|
||||
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applied = graphics_gitadora_apply_monitor_rect(
|
||||
graphics_gitadora_monitor_for_window_name(window_name),
|
||||
window_name,
|
||||
x,
|
||||
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
width,
|
||||
height,
|
||||
log_change);
|
||||
|
||||
if (window_name != "SMALL") {
|
||||
return applied;
|
||||
}
|
||||
|
||||
std::pair<uint32_t, uint32_t> result;
|
||||
if (GRAPHICS_GITADORA_SMALL_SIZE.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_SIZE.value(), result)) {
|
||||
width = result.first;
|
||||
height = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallsize");
|
||||
}
|
||||
}
|
||||
if (GRAPHICS_GITADORA_SMALL_POS.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_POS.value(), result)) {
|
||||
x = result.first;
|
||||
y = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallpos");
|
||||
}
|
||||
}
|
||||
|
||||
if (applied && log_change &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() || GRAPHICS_GITADORA_SMALL_POS.has_value())) {
|
||||
log_info(
|
||||
"graphics-windowed",
|
||||
"GITADORA SMALL custom override: pos=({}, {}), size={}x{}",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
void graphics_capture_initial_window(HWND hWnd) {
|
||||
@@ -254,7 +299,7 @@ void graphics_capture_initial_window(HWND hWnd) {
|
||||
cfg::SCREENRESIZE->init_client_width = client_w;
|
||||
cfg::SCREENRESIZE->init_client_height = client_h;
|
||||
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
|
||||
log_debug(
|
||||
log_misc(
|
||||
"graphics-windowed",
|
||||
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
|
||||
fmt::ptr(hWnd),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "external/nvapi/NvApiDriverSettings.h"
|
||||
#include "avs/game.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
@@ -12,14 +13,21 @@ namespace nvapi_hook {
|
||||
bool BYPASS_NVAPI = false;
|
||||
|
||||
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
|
||||
|
||||
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
|
||||
static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr;
|
||||
static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr;
|
||||
|
||||
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags);
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
|
||||
|
||||
void initialize(HINSTANCE dll) {
|
||||
|
||||
#ifdef SPICE64
|
||||
std::string nvapi_dll = "nvapi64.dll";
|
||||
#else
|
||||
@@ -40,6 +48,18 @@ namespace nvapi_hook {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// spoof display connector types only for SDVX
|
||||
if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) {
|
||||
NvAPI_GPU_GetConnectedDisplayIds_orig =
|
||||
reinterpret_cast<decltype(NvAPI_GPU_GetConnectedDisplayIds) *>(
|
||||
NvAPI_QueryInterface_orig(func_code));
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig =
|
||||
reinterpret_cast<decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *>(
|
||||
NvAPI_QueryInterface_orig(0x1E9D8A31));
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked");
|
||||
return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook;
|
||||
}
|
||||
|
||||
// NvAPI_DISP_SetDisplayConfig
|
||||
if (func_code == 0x5D8CF8DE) {
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
|
||||
@@ -51,8 +71,53 @@ namespace nvapi_hook {
|
||||
return NvAPI_QueryInterface_orig(func_code);
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags) {
|
||||
|
||||
if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) {
|
||||
return NVAPI_NO_IMPLEMENTATION;
|
||||
}
|
||||
|
||||
const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig(
|
||||
hPhysicalGpu,
|
||||
pDisplayIds,
|
||||
pDisplayIdCount,
|
||||
flags);
|
||||
|
||||
if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr ||
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) {
|
||||
return status;
|
||||
}
|
||||
|
||||
NvU32 primary_display_id = 0;
|
||||
if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// report the primary display adapter as DP and all other displays as HDMI
|
||||
// (the game expects this since the VM cab is like that)
|
||||
//
|
||||
// this doesn't actually affect gameplay; only how
|
||||
// NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz
|
||||
// gets printed.
|
||||
//
|
||||
// for the actual value that matters for gameplay, it's this:
|
||||
// NvDisplayConfig: GetMainDisplayRefreshRate = {}hz
|
||||
for (NvU32 index = 0; index < *pDisplayIdCount; index++) {
|
||||
auto &display = pDisplayIds[index];
|
||||
display.connectorType = display.displayId == primary_display_id ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
|
||||
// do not let the game apply its emulated cabinet topology to Windows
|
||||
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
#include "nvapi_impl.h"
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sysutils.h"
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
|
||||
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
|
||||
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
|
||||
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
|
||||
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
|
||||
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
|
||||
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
|
||||
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
|
||||
|
||||
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
|
||||
|
||||
struct SyntheticDisplay {
|
||||
NvU32 display_id;
|
||||
NvU32 width;
|
||||
NvU32 height;
|
||||
NvU32 color_depth;
|
||||
NvS32 x;
|
||||
NvS32 y;
|
||||
NvU32 refresh_rate_1k;
|
||||
NV_ROTATE rotation;
|
||||
bool primary;
|
||||
};
|
||||
|
||||
static bool provider_initialized = false;
|
||||
static bool nvapi_initialized = false;
|
||||
static int gpu_handle_storage = 0;
|
||||
// snapshot of the Win32 display state exposed through synthetic NVAPI
|
||||
static std::vector<SyntheticDisplay> displays;
|
||||
|
||||
static NvPhysicalGpuHandle get_gpu_handle() {
|
||||
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
|
||||
}
|
||||
|
||||
static NV_ROTATE get_rotation(DWORD orientation) {
|
||||
switch (orientation) {
|
||||
case DMDO_90:
|
||||
return NV_ROTATE_90;
|
||||
case DMDO_180:
|
||||
return NV_ROTATE_180;
|
||||
case DMDO_270:
|
||||
return NV_ROTATE_270;
|
||||
default:
|
||||
return NV_ROTATE_0;
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<SyntheticDisplay> enumerate_displays(
|
||||
uint32_t main_refresh_hz,
|
||||
uint32_t sub_refresh_hz) {
|
||||
|
||||
std::vector<SyntheticDisplay> result;
|
||||
|
||||
// reuse the active monitor list, then read live modes after -mainmonitor changes
|
||||
for (const auto &monitor : sysutils::enumerate_monitors()) {
|
||||
DEVMODEA mode {};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (!EnumDisplaySettingsExA(
|
||||
monitor.display_name.c_str(),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mode,
|
||||
0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = mode.dmPelsWidth,
|
||||
.height = mode.dmPelsHeight,
|
||||
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
|
||||
.x = mode.dmPosition.x,
|
||||
.y = mode.dmPosition.y,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = get_rotation(mode.dmDisplayOrientation),
|
||||
.primary = primary,
|
||||
});
|
||||
}
|
||||
|
||||
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
|
||||
return left.primary && !right.primary;
|
||||
});
|
||||
|
||||
if (result.size() > 2) {
|
||||
result.resize(2);
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
.color_depth = 32,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = NV_ROTATE_0,
|
||||
.primary = true,
|
||||
});
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < result.size(); index++) {
|
||||
auto &display = result[index];
|
||||
display.primary = index == 0;
|
||||
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
|
||||
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
|
||||
display.refresh_rate_1k = refresh_hz * 1000;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// initializes NVAPI for the calling process.
|
||||
// marks the synthetic provider initialized without contacting a driver.
|
||||
static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Initialize");
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// initializes NVAPI with additional client flags.
|
||||
// accepts the flags and marks the synthetic provider initialized.
|
||||
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
|
||||
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// releases NVAPI state held for the calling process.
|
||||
// clears the synthetic initialization state while leaving the provider installed.
|
||||
static NvAPI_Status __cdecl NvAPI_Unload_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Unload");
|
||||
nvapi_initialized = false;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// enumerates physical GPU handles managed by the NVIDIA driver.
|
||||
// returns one stable synthetic GPU containing all exposed displays.
|
||||
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
|
||||
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
|
||||
NvU32 *gpu_count) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs(handles={}, count={})",
|
||||
fmt::ptr(gpu_handles),
|
||||
fmt::ptr(gpu_count));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handles == nullptr || gpu_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
gpu_handles[0] = get_gpu_handle();
|
||||
*gpu_count = 1;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
|
||||
fmt::ptr(gpu_handles[0]),
|
||||
*gpu_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns connected display descriptors for a physical GPU.
|
||||
// exposes the monitor snapshot as DP primary and HDMI secondary displays.
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
|
||||
NvPhysicalGpuHandle gpu_handle,
|
||||
NV_GPU_DISPLAYIDS *display_ids,
|
||||
NvU32 *display_id_count,
|
||||
NvU32 flags) {
|
||||
|
||||
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
|
||||
fmt::ptr(gpu_handle),
|
||||
fmt::ptr(display_ids),
|
||||
input_count,
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handle != get_gpu_handle()) {
|
||||
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
|
||||
}
|
||||
if (display_id_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (display_ids == nullptr) {
|
||||
*display_id_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *display_id_count;
|
||||
*display_id_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
const auto &source = displays[index];
|
||||
auto &destination = display_ids[index];
|
||||
destination = {};
|
||||
destination.version = NV_GPU_DISPLAYIDS_VER;
|
||||
destination.connectorType = source.primary ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
destination.displayId = source.display_id;
|
||||
destination.isActive = 1;
|
||||
destination.isOSVisible = 1;
|
||||
destination.isConnected = 1;
|
||||
destination.isPhysicallyConnected = 1;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns the NVAPI display ID associated with the Windows GDI primary.
|
||||
// returns the first synthetic display, ordered from the live desktop origin.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
|
||||
fmt::ptr(display_id));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (display_id == nullptr || displays.empty()) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
*display_id = displays.front().display_id;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
|
||||
*display_id);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
static void fill_source_mode(
|
||||
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
|
||||
const SyntheticDisplay &source) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
*destination = {};
|
||||
destination->resolution.width = source.width;
|
||||
destination->resolution.height = source.height;
|
||||
destination->resolution.colorDepth = source.color_depth;
|
||||
destination->colorFormat = NV_FORMAT_A8R8G8B8;
|
||||
destination->position.x = source.x;
|
||||
destination->position.y = source.y;
|
||||
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
|
||||
destination->bGDIPrimary = source.primary ? 1 : 0;
|
||||
}
|
||||
|
||||
static NvAPI_Status fill_target(
|
||||
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
|
||||
const SyntheticDisplay &source,
|
||||
NvU32 target_id) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
auto *details = destination->details;
|
||||
destination->displayId = source.display_id;
|
||||
destination->targetId = target_id;
|
||||
|
||||
if (details == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
|
||||
*details = {};
|
||||
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
|
||||
details->rotation = source.rotation;
|
||||
details->scaling = NV_SCALING_DEFAULT;
|
||||
details->refreshRate1K = source.refresh_rate_1k;
|
||||
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// retrieves the current global display topology through NVAPI's three-pass contract.
|
||||
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
|
||||
NvU32 *path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info) {
|
||||
|
||||
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
|
||||
input_count,
|
||||
fmt::ptr(path_info));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (path_info_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (path_info == nullptr) {
|
||||
*path_info_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *path_info_count;
|
||||
*path_info_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
auto &path = path_info[index];
|
||||
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
const auto &display = displays[index];
|
||||
path.sourceId = index;
|
||||
path.targetInfoCount = 1;
|
||||
path.IsNonNVIDIAAdapter = 0;
|
||||
path.pOSAdapterID = nullptr;
|
||||
fill_source_mode(path.sourceModeInfo, display);
|
||||
|
||||
const NvAPI_Status status = fill_target(path.targetInfo, display, index);
|
||||
if (status != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// applies a supplied global display topology through the NVIDIA driver.
|
||||
// accepts the cabinet topology without making any changes to Windows.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
|
||||
NvU32 path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info,
|
||||
NvU32 flags) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
|
||||
path_info_count,
|
||||
fmt::ptr(path_info),
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static uintptr_t *query_result(T function) {
|
||||
return reinterpret_cast<uintptr_t *>(function);
|
||||
}
|
||||
|
||||
// resolves an NVAPI function ID to its implementation address.
|
||||
// exposes only the synthetic entry points used by KFC and rejects all others.
|
||||
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
|
||||
uintptr_t *result = nullptr;
|
||||
switch (function_id) {
|
||||
case NVAPI_INITIALIZE_ID:
|
||||
result = query_result(NvAPI_Initialize_impl);
|
||||
break;
|
||||
case NVAPI_INITIALIZE_EX_ID:
|
||||
result = query_result(NvAPI_InitializeEx_impl);
|
||||
break;
|
||||
case NVAPI_UNLOAD_ID:
|
||||
result = query_result(NvAPI_Unload_impl);
|
||||
break;
|
||||
case NVAPI_ENUM_PHYSICAL_GPUS_ID:
|
||||
result = query_result(NvAPI_EnumPhysicalGPUs_impl);
|
||||
break;
|
||||
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
|
||||
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
|
||||
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
|
||||
break;
|
||||
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_QueryInterface(0x{:x}) - {}",
|
||||
function_id,
|
||||
result != nullptr ? "implemented" : "unsupported");
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
|
||||
if (provider_initialized) {
|
||||
return true;
|
||||
}
|
||||
if (dll == nullptr) {
|
||||
log_warning("nvapi_impl", "invalid synthetic module handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
|
||||
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
|
||||
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
|
||||
libraryhook_enable();
|
||||
|
||||
provider_initialized = true;
|
||||
log_info(
|
||||
"nvapi_impl",
|
||||
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
|
||||
NVAPI_DLL_NAME_A,
|
||||
displays.size(),
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,7 @@ static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
||||
|
||||
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
|
||||
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
|
||||
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
|
||||
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
|
||||
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
|
||||
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
|
||||
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
|
||||
return LoadLibraryW_orig(lpFileName);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
|
||||
|
||||
// check hooks
|
||||
if (lpFileName) {
|
||||
auto module = LIBRARIES_W.find(lpFileName);
|
||||
if (module != LIBRARIES_W.end()) {
|
||||
return module->second;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
||||
|
||||
// check hooks
|
||||
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
|
||||
// detour
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/hpm/hpm.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/iidx/poke.h"
|
||||
#ifdef SPICE64
|
||||
#include "games/iidx/camera.h"
|
||||
#endif
|
||||
#include "games/iidx/poke.h"
|
||||
#include "games/jb/jb.h"
|
||||
#include "games/mga/mga.h"
|
||||
#include "games/nost/nost.h"
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/qma/qma.h"
|
||||
#include "games/rb/rb.h"
|
||||
#include "games/rb/touch_debug.h"
|
||||
#include "games/rf3d/rf3d.h"
|
||||
#include "games/sc/sc.h"
|
||||
#include "games/scotto/scotto.h"
|
||||
@@ -101,7 +102,7 @@
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "overlay/windows/patch_manager.h"
|
||||
#include "patcher/patch_manager.h"
|
||||
#include "overlay/windows/iidx_seg.h"
|
||||
#include "overlay/windows/obs.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
@@ -378,6 +379,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
@@ -427,9 +429,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
||||
D3D9_DEVICE_HOOK_DISABLE = true;
|
||||
// touch emulation gets disabled, might as well turn these on
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
|
||||
overlay::ENABLED = false;
|
||||
// leaving these on without dx9hooks result in torn state and therefore failure to draw
|
||||
@@ -489,9 +488,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
|
||||
attach_sdvx = true;
|
||||
}
|
||||
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
|
||||
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
|
||||
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
|
||||
@@ -592,9 +588,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
|
||||
games::iidx::DISABLE_ESPEC_IO = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNativeTouch].value_bool()) {
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
}
|
||||
// should come later since this will override a few settings
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
|
||||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
|
||||
@@ -642,9 +635,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
|
||||
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
||||
games::popn::NATIVE_TOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||
SUBSCREEN_FORCE_REDRAW = true;
|
||||
}
|
||||
@@ -888,7 +878,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
||||
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
options[launcher::Options::PatchManagerConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
||||
@@ -1064,6 +1054,15 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||
rawinput::touch::INVERTED = true;
|
||||
}
|
||||
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
|
||||
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
|
||||
if (mode == "on") {
|
||||
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
|
||||
} else if (mode == "off") {
|
||||
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
|
||||
}
|
||||
// "auto" leaves the default (per-game)
|
||||
}
|
||||
// DisableTouchCardInsert is no longer honored in spice2x
|
||||
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
||||
// SPICETOUCH_CARD_DISABLE = true;
|
||||
@@ -1105,6 +1104,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
||||
overlay::ENABLED = false;
|
||||
}
|
||||
if (options[launcher::Options::OverlayKeyboardNavigation].value_bool()) {
|
||||
overlay::ENABLE_KEYBOARD_NAVIGATION = true;
|
||||
}
|
||||
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
||||
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
||||
if (10 <= val && val <= 400 && val != 100) {
|
||||
@@ -1273,6 +1275,14 @@ int main_implementation(int argc, char *argv[]) {
|
||||
GRAPHICS_GITADORA_SMALL_MONITOR =
|
||||
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallSize].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_SIZE =
|
||||
options[launcher::Options::GitaDoraWindowedSmallSize].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallPosition].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_POS =
|
||||
options[launcher::Options::GitaDoraWindowedSmallPosition].value_text();
|
||||
}
|
||||
|
||||
// IIDX/SDVX Windowed Subscreen
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
|
||||
@@ -1305,21 +1315,36 @@ int main_implementation(int argc, char *argv[]) {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
|
||||
} else {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||
}
|
||||
}
|
||||
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
|
||||
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
|
||||
if (mode == "none") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
|
||||
} else if (mode == "box") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
|
||||
} else if (mode == "all") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
|
||||
} else {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
|
||||
}
|
||||
}
|
||||
|
||||
if (options[launcher::Options::JubeatTouchDebounce].is_active()) {
|
||||
games::jb::TOUCH_DEBOUNCE_MS =
|
||||
options[launcher::Options::JubeatTouchDebounce].value_uint32();
|
||||
}
|
||||
|
||||
// reflec beat touch emulation
|
||||
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
|
||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
||||
}
|
||||
if (options[launcher::Options::RBTouchSize].is_active()) {
|
||||
const auto text = options[launcher::Options::RBTouchSize].value_text();
|
||||
if (text == "3") {
|
||||
games::rb::TOUCH_SIZE = 3;
|
||||
}
|
||||
}
|
||||
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
|
||||
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
||||
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
||||
}
|
||||
@@ -1758,10 +1783,19 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
// enable subscreen touch emulation
|
||||
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
||||
games::iidx::poke::enable();
|
||||
games::iidx::ENABLE_POKE = true;
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
||||
games::nost::ENABLE_POKE = TRUE;
|
||||
games::nost::ENABLE_POKE = true;
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaTouchMode].is_active()) {
|
||||
if (overlay::ENABLED) {
|
||||
games::nost::ENABLE_TOUCH_MODE = true;
|
||||
} else {
|
||||
log_warning(
|
||||
"launcher",
|
||||
"Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2489,10 +2523,8 @@ int main_implementation(int argc, char *argv[]) {
|
||||
// copy defaults to nvram
|
||||
avs::core::copy_defaults();
|
||||
|
||||
// prepare patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, false);
|
||||
}
|
||||
// prepare patches (registers the DLL-load notification before the game DLL loads)
|
||||
patcher::init();
|
||||
|
||||
// load game
|
||||
avs::game::load_dll();
|
||||
@@ -2620,9 +2652,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
// apply patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, true);
|
||||
}
|
||||
patcher::apply_patches_on_start();
|
||||
|
||||
// load AVS-EA3
|
||||
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
||||
|
||||
@@ -434,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Advanced Network",
|
||||
},
|
||||
{
|
||||
.title = "Preferred NetAdapter IP",
|
||||
.title = "Preferred Network Adapter's IP",
|
||||
.name = "network",
|
||||
.desc = "This is NOT the EA service URL; use -url for that. "
|
||||
"Force the use of an adapter with the specified network. Must also provide -subnet.",
|
||||
.display_name = "netadapterip",
|
||||
.aliases = "netadapterip",
|
||||
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||
"with the specified IP address. You must also set -netadaptersubnet.",
|
||||
.type = OptionType::Text,
|
||||
.category = "Advanced Network",
|
||||
.sensitive = true,
|
||||
},
|
||||
{
|
||||
.title = "Preferred NetAdapter Subnet",
|
||||
.title = "Preferred Network Adapter's Subnet",
|
||||
.name = "subnet",
|
||||
.desc = "Force the use of an adapter with the specified subnet. Must also provide -network.",
|
||||
.display_name = "netadaptersubnet",
|
||||
.aliases = "netadaptersubnet",
|
||||
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||
"with the specified subnet. You must also set -netadapterip.",
|
||||
.type = OptionType::Text,
|
||||
.category = "Advanced Network",
|
||||
},
|
||||
@@ -505,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
{
|
||||
.title = "Overlay Keyboard Navigation",
|
||||
.name = "keyboardnav",
|
||||
.desc = "Enables keyboard navigation in the in-game overlay.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
{
|
||||
// OverlayScaling
|
||||
.title = "Spice Overlay UI Scale %",
|
||||
@@ -950,13 +962,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Cab Peripherals",
|
||||
},
|
||||
{
|
||||
.title = "SDVX FS Subscreen Native Touch Handling",
|
||||
.title = "SDVX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sdvxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -1031,16 +1042,16 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||
.name = "sdvxnolive2d",
|
||||
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||
"Off: leave Live2D as-is.\n"
|
||||
"Always: never render Live2D (also removes the menu navigator).\n"
|
||||
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||
"off: leave Live2D as-is.\n"
|
||||
"always: hide Live2D (also hides menu navigator).\n"
|
||||
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
.elements = {
|
||||
{"off", "Show Live2D"},
|
||||
{"always", "Never show Live2D"},
|
||||
{"ingame", "Show navigators only"},
|
||||
{"always", "Hide Live2D"},
|
||||
{"ingame", "Hide during songs"},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
@@ -1147,13 +1158,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// PopnNativeTouch
|
||||
.title = "Pop'n Music PikaPika Native Touch Handling",
|
||||
.title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "popnnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Pop'n Music",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -1772,19 +1782,19 @@ 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",
|
||||
},
|
||||
{
|
||||
.title = "Force Touch Emulation",
|
||||
.title = "Force Legacy Touch Emulation",
|
||||
.name = "touchemuforce",
|
||||
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
||||
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
|
||||
"Do not enable this unless you have trouble with using a mouse to emulate touch input.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
@@ -1796,6 +1806,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
{
|
||||
// RawInputTouchAspectRatio
|
||||
.title = "Raw Input Touch Fix Aspect Ratio",
|
||||
.name = "rawtouchaspect",
|
||||
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
|
||||
"different aspect ratio than the touch panel's native resolution. Only affects the default "
|
||||
"raw input touch handler. This is automatically enabled, only force on or off if you have trouble.\n\n"
|
||||
"auto (default): automatically enable on a per-game basis.\n"
|
||||
"on: forced on\n"
|
||||
"off: forced off",
|
||||
.type = OptionType::Enum,
|
||||
.category = "Touch Parameters",
|
||||
.elements = {
|
||||
{"auto", ""},
|
||||
{"on", ""},
|
||||
{"off", ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
// DisableTouchCardInsert
|
||||
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
||||
@@ -1807,12 +1835,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// spice2x_TouchCardInsert
|
||||
.title = "Show Insert Card button",
|
||||
.title = "Show Insert Card button (DEPRECATED)",
|
||||
.name = "sp2x-touchcard",
|
||||
.display_name = "touchcard",
|
||||
.aliases= "touchcard",
|
||||
.desc = "Show Insert Card touch button on main display.",
|
||||
.desc = "Show Insert Card touch button on main display. "
|
||||
"DEPRECATED - only works in very specific situations (jubeat + wintouch)",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
{
|
||||
@@ -1884,7 +1914,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.title = "HID SmartCard Fix UID",
|
||||
.name = "scardfix",
|
||||
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
||||
"legacy: Preserve buggy old behavior (Default)\n\n"
|
||||
"legacy (default): Preserve buggy old behavior\n\n"
|
||||
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
||||
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
||||
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
||||
@@ -2612,6 +2642,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Game Windowed Settings",
|
||||
.picker = OptionPickerType::Monitor,
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallSize
|
||||
.title = "GitaDora Windowed SMALL Size",
|
||||
.name = "gdwsmallsize",
|
||||
.desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "800,1280",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallPosition
|
||||
.title = "GitaDora Windowed SMALL Position",
|
||||
.name = "gdwsmallpos",
|
||||
.desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "0,0",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXWindowedSubscreenSize
|
||||
.title = "IIDX Windowed Subscreen Size",
|
||||
@@ -2668,6 +2718,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
|
||||
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
@@ -2675,10 +2726,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.elements = {
|
||||
{"legacy", ""},
|
||||
{"improved", ""},
|
||||
{"plus", ""},
|
||||
{"accurate", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// JubeatTouchDebug
|
||||
.title = "JB Touch Debug Overlay",
|
||||
.name = "jubeattouchdebug",
|
||||
.desc = "For touch screen players: draw a debug overlay on the main display. "
|
||||
"Requires the Spice Overlay to be enabled.\n\n"
|
||||
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
|
||||
"none - draw nothing\n\n"
|
||||
"box - show the 4x4 touch boundary boxes\n\n"
|
||||
"all - show both the boxes and the touch circles",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
.category = "Game Options",
|
||||
.elements = {
|
||||
{"auto", ""},
|
||||
{"none", ""},
|
||||
{"box", ""},
|
||||
{"all", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// JubeatTouchDebounce
|
||||
.title = "JB Touch Debounce",
|
||||
.name = "jubeattouchdebounce",
|
||||
.desc = "For touch screen players: ignore extremely quick touches by requiring a "
|
||||
"touch to be held for at least this many milliseconds before it registers.\n\n"
|
||||
"Useful for filtering phantom touches on a noisy touch screen. A higher value "
|
||||
"adds input latency, so keep it small. Default: off (0).",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "8",
|
||||
.game_name = "Jubeat",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// RBTouchDebug
|
||||
.title = "RB Touch Debug Overlay",
|
||||
.name = "rbtouchdebug",
|
||||
.desc = "Draw lines to show IR sensor emulation state.\n\n"
|
||||
"Note: lines will not perfectly align with touches; this is by design, it's showing the "
|
||||
"IR sensor state, before the game applies its own touch detection algorithm.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Reflec Beat",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// spice2x_RBTouchScale
|
||||
.title = "RB Touch Emulation Scale",
|
||||
@@ -2693,14 +2792,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// RBTouchSize
|
||||
.title = "RB Touch Emulation Size",
|
||||
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
|
||||
.name = "rbtouchsize",
|
||||
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
|
||||
.desc = "This option is deprecated and no longer has any effect. "
|
||||
"Reflec Beat touch emulation always uses the 3x3 sensor model.",
|
||||
.type = OptionType::Enum,
|
||||
.hidden = true,
|
||||
.game_name = "Reflec Beat",
|
||||
.category = "Game Options",
|
||||
.elements = {
|
||||
{"1", "1x1"},
|
||||
{"3", "3x3"},
|
||||
},
|
||||
},
|
||||
@@ -2708,9 +2808,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
// RBTouchPollRate
|
||||
.title = "RB Touch Emulation Poll Hz",
|
||||
.name = "rbtouchhz",
|
||||
.desc = "By default, the game polls for touch at 120Hz. "
|
||||
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
|
||||
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
|
||||
.desc = "By default, the game polls for touch at ~125Hz. "
|
||||
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
|
||||
"Higher rates reduce input latency (the game sees a fresher touch position) "
|
||||
"but do NOT improve spatial accuracy, and very high rates just waste CPU.",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "250",
|
||||
.game_name = "Reflec Beat",
|
||||
@@ -2787,15 +2888,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXNativeTouch
|
||||
.title = "IIDX TDJ Subscreen Native Touch Handling",
|
||||
.title = "IIDX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sp2x-iidxnativetouch",
|
||||
.display_name = "iidxnativetouch",
|
||||
.aliases= "iidxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Beatmania IIDX",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -3049,6 +3149,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// NostalgiaTouchMode
|
||||
.title = "Nostalgia Touch Piano",
|
||||
.name = "nosttouch",
|
||||
.desc =
|
||||
"Allows you to play the piano by touching the screen instead of a controller. "
|
||||
"Use the mode switch button to toggle between interacting with the menu and playing the piano.\n\n"
|
||||
"Velocity sensitivity is not supported. Touch targets will be pixel-perfect aligned with the "
|
||||
"judgement line, key beams, and notes.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// ForceBackBufferCount
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace launcher {
|
||||
SOFTID,
|
||||
VREnable,
|
||||
DisableOverlay,
|
||||
OverlayKeyboardNavigation,
|
||||
OverlayScaling,
|
||||
NotificationPosition,
|
||||
spice2x_FpsAutoShow,
|
||||
@@ -179,6 +180,7 @@ namespace launcher {
|
||||
ForceWinTouch,
|
||||
ForceTouchEmulation,
|
||||
InvertTouchCoordinates,
|
||||
RawInputTouchAspectRatio,
|
||||
DisableTouchCardInsert,
|
||||
spice2x_TouchCardInsert,
|
||||
ICCAReaderPort,
|
||||
@@ -256,12 +258,17 @@ namespace launcher {
|
||||
GitaDoraWindowedLeftMonitor,
|
||||
GitaDoraWindowedRightMonitor,
|
||||
GitaDoraWindowedSmallMonitor,
|
||||
GitaDoraWindowedSmallSize,
|
||||
GitaDoraWindowedSmallPosition,
|
||||
spice2x_IIDXWindowedSubscreenSize,
|
||||
spice2x_IIDXWindowedSubscreenPosition,
|
||||
IIDXWindowedSubscreenBorderless,
|
||||
IIDXWindowedSubscreenAlwaysOnTop,
|
||||
spice2x_JubeatLegacyTouch,
|
||||
JubeatTouchAlgo,
|
||||
JubeatTouchDebug,
|
||||
JubeatTouchDebounce,
|
||||
RBTouchDebug,
|
||||
spice2x_RBTouchScale,
|
||||
RBTouchSize,
|
||||
RBTouchPollRate,
|
||||
@@ -290,6 +297,7 @@ namespace launcher {
|
||||
DDRP4IOBufferMode,
|
||||
InputRequiresFocus,
|
||||
NostalgiaPoke,
|
||||
NostalgiaTouchMode,
|
||||
ForceBackBufferCount,
|
||||
SDVXWindowedSubscreenSize,
|
||||
SDVXWindowedSubscreenPosition,
|
||||
|
||||
+401
-45
@@ -1,5 +1,6 @@
|
||||
#include "signal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <exception>
|
||||
|
||||
@@ -25,6 +26,7 @@
|
||||
#endif
|
||||
|
||||
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
|
||||
static LONG EXCEPTION_HANDLER_ACTIVE = 0;
|
||||
|
||||
namespace launcher::signal {
|
||||
|
||||
@@ -47,7 +49,7 @@ static std::string control_code(DWORD dwCtrlType) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
static std::string exception_code(const struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
V(EXCEPTION_ACCESS_VIOLATION);
|
||||
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||
@@ -77,6 +79,388 @@ static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
|
||||
#undef V
|
||||
|
||||
static const char *access_operation(ULONG_PTR operation) {
|
||||
switch (operation) {
|
||||
case 0:
|
||||
return "read";
|
||||
case 1:
|
||||
return "write";
|
||||
case 8:
|
||||
return "execute";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_state(DWORD state) {
|
||||
switch (state) {
|
||||
case MEM_COMMIT:
|
||||
return "committed";
|
||||
case MEM_FREE:
|
||||
return "free";
|
||||
case MEM_RESERVE:
|
||||
return "reserved";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_type(DWORD type) {
|
||||
switch (type) {
|
||||
case MEM_IMAGE:
|
||||
return "image";
|
||||
case MEM_MAPPED:
|
||||
return "mapped";
|
||||
case MEM_PRIVATE:
|
||||
return "private";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_protection(DWORD protection) {
|
||||
switch (protection & 0xff) {
|
||||
case PAGE_EXECUTE:
|
||||
return "execute";
|
||||
case PAGE_EXECUTE_READ:
|
||||
return "execute-read";
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
return "execute-read-write";
|
||||
case PAGE_EXECUTE_WRITECOPY:
|
||||
return "execute-write-copy";
|
||||
case PAGE_NOACCESS:
|
||||
return "no-access";
|
||||
case PAGE_READONLY:
|
||||
return "read-only";
|
||||
case PAGE_READWRITE:
|
||||
return "read-write";
|
||||
case PAGE_WRITECOPY:
|
||||
return "write-copy";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_parameters(const struct _EXCEPTION_RECORD *record) {
|
||||
const auto parameter_count = record->NumberParameters <= EXCEPTION_MAXIMUM_PARAMETERS ?
|
||||
record->NumberParameters : EXCEPTION_MAXIMUM_PARAMETERS;
|
||||
|
||||
log_warning("signal", "exception flags: 0x{:08x}, parameters: {}",
|
||||
record->ExceptionFlags, parameter_count);
|
||||
|
||||
for (DWORD parameter = 0; parameter < parameter_count; parameter++) {
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "exception parameter[{}]: {:016x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#else
|
||||
log_warning("signal", "exception parameter[{}]: {:08x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static bool query_memory(const void *address, MEMORY_BASIC_INFORMATION *memory) {
|
||||
return VirtualQuery(address, memory, sizeof(*memory)) == sizeof(*memory);
|
||||
}
|
||||
|
||||
static void log_memory_information(const char *label, const MEMORY_BASIC_INFORMATION &memory) {
|
||||
log_warning("signal",
|
||||
"{}: base={}, size=0x{:x}, allocation_base={}, state={} (0x{:x}), "
|
||||
"protect={} (0x{:x}), type={} (0x{:x})",
|
||||
label,
|
||||
fmt::ptr(memory.BaseAddress),
|
||||
memory.RegionSize,
|
||||
fmt::ptr(memory.AllocationBase),
|
||||
memory_state(memory.State),
|
||||
memory.State,
|
||||
memory_protection(memory.Protect),
|
||||
memory.Protect,
|
||||
memory_type(memory.Type),
|
||||
memory.Type);
|
||||
}
|
||||
|
||||
static void log_memory_region(const char *label, const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory)) {
|
||||
log_warning("signal", "{}: VirtualQuery failed for {}: 0x{:08x}",
|
||||
label, fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_memory_information(label, memory);
|
||||
}
|
||||
|
||||
static void log_adjacent_memory_regions(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION current {};
|
||||
if (!query_memory(address, ¤t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto current_base = reinterpret_cast<uintptr_t>(current.BaseAddress);
|
||||
if (current_base > 0) {
|
||||
MEMORY_BASIC_INFORMATION previous {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_base - 1), &previous)) {
|
||||
log_memory_information("fault memory previous", previous);
|
||||
}
|
||||
}
|
||||
|
||||
const auto current_end = current_base + current.RegionSize;
|
||||
if (current_end > current_base) {
|
||||
MEMORY_BASIC_INFORMATION next {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_end), &next)) {
|
||||
log_memory_information("fault memory next", next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_module(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) || memory.Type != MEM_IMAGE || memory.AllocationBase == nullptr) {
|
||||
log_warning("signal", "exception module: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
char module_path[MAX_PATH] {};
|
||||
const auto module = static_cast<HMODULE>(memory.AllocationBase);
|
||||
const auto module_base = reinterpret_cast<uintptr_t>(memory.AllocationBase);
|
||||
const auto exception_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto module_length = GetModuleFileNameA(module, module_path, sizeof(module_path));
|
||||
|
||||
if (module_length > 0) {
|
||||
const std::string module_name(module_path, std::min<size_t>(module_length, sizeof(module_path)));
|
||||
log_warning("signal", "exception module: '{}' base={}, rva=0x{:x}",
|
||||
module_name, fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
} else {
|
||||
log_warning("signal", "exception module: base={}, rva=0x{:x}",
|
||||
fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_instruction_bytes(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) ||
|
||||
memory.State != MEM_COMMIT ||
|
||||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
|
||||
log_warning("signal", "instruction bytes: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr size_t max_instruction_bytes = 16;
|
||||
const auto region_end = reinterpret_cast<uintptr_t>(memory.BaseAddress) + memory.RegionSize;
|
||||
const auto instruction_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto available = region_end > instruction_address ? region_end - instruction_address : 0;
|
||||
const auto byte_count = std::min(max_instruction_bytes, static_cast<size_t>(available));
|
||||
uint8_t bytes[max_instruction_bytes] {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (byte_count == 0 ||
|
||||
!ReadProcessMemory(GetCurrentProcess(), address, bytes, byte_count, &bytes_read)) {
|
||||
log_warning("signal", "instruction bytes: read failed at {}: 0x{:08x}",
|
||||
fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string byte_string;
|
||||
byte_string.reserve(bytes_read * 3);
|
||||
|
||||
for (size_t index = 0; index < bytes_read; index++) {
|
||||
fmt::format_to(std::back_inserter(byte_string), "{:02x}{}",
|
||||
bytes[index], index + 1 < bytes_read ? " " : "");
|
||||
}
|
||||
|
||||
log_warning("signal", "instruction bytes: {}", byte_string);
|
||||
}
|
||||
|
||||
static void log_stack_context(const CONTEXT *context) {
|
||||
const auto *tib = reinterpret_cast<const NT_TIB *>(NtCurrentTeb());
|
||||
const auto stack_base = reinterpret_cast<uintptr_t>(tib->StackBase);
|
||||
const auto stack_limit = reinterpret_cast<uintptr_t>(tib->StackLimit);
|
||||
#ifdef _WIN64
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Rsp);
|
||||
#else
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Esp);
|
||||
#endif
|
||||
const auto stack_used = stack_base > stack_pointer ? stack_base - stack_pointer : 0;
|
||||
const auto stack_remaining = stack_pointer > stack_limit ? stack_pointer - stack_limit : 0;
|
||||
|
||||
log_warning("signal", "stack: base={}, limit={}, pointer={}, used=0x{:x}, remaining=0x{:x}",
|
||||
fmt::ptr(tib->StackBase),
|
||||
fmt::ptr(tib->StackLimit),
|
||||
fmt::ptr(reinterpret_cast<const void *>(stack_pointer)),
|
||||
stack_used,
|
||||
stack_remaining);
|
||||
}
|
||||
|
||||
static void log_exception_context(struct _EXCEPTION_POINTERS *ExceptionInfo, DWORD last_error) {
|
||||
const auto *record = ExceptionInfo->ExceptionRecord;
|
||||
const auto *context = ExceptionInfo->ContextRecord;
|
||||
|
||||
log_warning("signal", "thread id: {}", GetCurrentThreadId());
|
||||
log_warning("signal", "thread last error (GetLastError()) at exception: 0x{:08x}", last_error);
|
||||
log_warning("signal", "exception address: {}", fmt::ptr(record->ExceptionAddress));
|
||||
log_exception_module(record->ExceptionAddress);
|
||||
log_instruction_bytes(record->ExceptionAddress);
|
||||
log_exception_parameters(record);
|
||||
|
||||
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
|
||||
record->NumberParameters >= 2) {
|
||||
const auto operation = record->ExceptionInformation[0];
|
||||
const auto address = reinterpret_cast<const void *>(record->ExceptionInformation[1]);
|
||||
log_warning("signal", "invalid memory access: {} at {}",
|
||||
access_operation(operation), fmt::ptr(address));
|
||||
log_memory_region("fault memory", address);
|
||||
log_adjacent_memory_regions(address);
|
||||
}
|
||||
|
||||
if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) {
|
||||
log_warning("signal", "in-page error status: 0x{:08x}",
|
||||
static_cast<DWORD>(record->ExceptionInformation[2]));
|
||||
}
|
||||
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_stack_context(context);
|
||||
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "registers: rax={:016x} rbx={:016x} rcx={:016x} rdx={:016x}",
|
||||
context->Rax, context->Rbx, context->Rcx, context->Rdx);
|
||||
log_warning("signal", "registers: rsi={:016x} rdi={:016x} rbp={:016x} rsp={:016x}",
|
||||
context->Rsi, context->Rdi, context->Rbp, context->Rsp);
|
||||
log_warning("signal", "registers: r8={:016x} r9={:016x} r10={:016x} r11={:016x}",
|
||||
context->R8, context->R9, context->R10, context->R11);
|
||||
log_warning("signal", "registers: r12={:016x} r13={:016x} r14={:016x} r15={:016x}",
|
||||
context->R12, context->R13, context->R14, context->R15);
|
||||
log_warning("signal", "registers: rip={:016x} eflags={:08x}",
|
||||
context->Rip, context->EFlags);
|
||||
#else
|
||||
log_warning("signal", "registers: eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}",
|
||||
context->Eax, context->Ebx, context->Ecx, context->Edx);
|
||||
log_warning("signal", "registers: esi={:08x} edi={:08x} ebp={:08x} esp={:08x}",
|
||||
context->Esi, context->Edi, context->Ebp, context->Esp);
|
||||
log_warning("signal", "registers: eip={:08x} eflags={:08x}",
|
||||
context->Eip, context->EFlags);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void write_minidump(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
if (MiniDumpWriteDump_local == nullptr) {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file == INVALID_HANDLE_VALUE) {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
constexpr auto extended_type = static_cast<MINIDUMP_TYPE>(
|
||||
MiniDumpWithUnloadedModules |
|
||||
MiniDumpWithIndirectlyReferencedMemory |
|
||||
MiniDumpWithProcessThreadData |
|
||||
MiniDumpWithFullMemoryInfo |
|
||||
MiniDumpWithThreadInfo |
|
||||
MiniDumpIgnoreInaccessibleMemory);
|
||||
|
||||
auto written_type = extended_type;
|
||||
auto minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
extended_type,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
auto minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
|
||||
if (!minidump_written) {
|
||||
log_warning("signal", "failed to write extended minidump: 0x{:08x}; retrying MiniDumpNormal",
|
||||
minidump_error);
|
||||
|
||||
LARGE_INTEGER file_start {};
|
||||
if (SetFilePointerEx(minidump_file, file_start, nullptr, FILE_BEGIN) &&
|
||||
SetEndOfFile(minidump_file)) {
|
||||
written_type = MiniDumpNormal;
|
||||
minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
} else {
|
||||
minidump_error = GetLastError();
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_INTEGER minidump_size {};
|
||||
const auto minidump_size_available = minidump_written &&
|
||||
GetFileSizeEx(minidump_file, &minidump_size);
|
||||
CloseHandle(minidump_file);
|
||||
|
||||
if (minidump_written) {
|
||||
if (minidump_size_available) {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x}, size={} bytes)",
|
||||
static_cast<unsigned>(written_type), minidump_size.QuadPart);
|
||||
} else {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x})",
|
||||
static_cast<unsigned>(written_type));
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "failed to write 'minidump.dmp': 0x{:08x}", minidump_error);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_chain(const struct _EXCEPTION_RECORD *record) {
|
||||
constexpr size_t max_exception_chain_depth = 8;
|
||||
auto *record_cause = record->ExceptionRecord;
|
||||
size_t depth = 0;
|
||||
|
||||
while (record_cause != nullptr && depth < max_exception_chain_depth) {
|
||||
struct _EXCEPTION_RECORD cause {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (!ReadProcessMemory(
|
||||
GetCurrentProcess(),
|
||||
record_cause,
|
||||
&cause,
|
||||
sizeof(cause),
|
||||
&bytes_read) ||
|
||||
bytes_read != sizeof(cause)) {
|
||||
log_warning("signal", "failed to read exception chain record {} at {}: 0x{:08x}",
|
||||
depth, fmt::ptr(record_cause), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_warning("signal", "caused by: {} at {}",
|
||||
exception_code(&cause), fmt::ptr(cause.ExceptionAddress));
|
||||
log_exception_parameters(&cause);
|
||||
record_cause = cause.ExceptionRecord;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (record_cause != nullptr) {
|
||||
log_warning("signal", "exception chain truncated after {} records", max_exception_chain_depth);
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
|
||||
|
||||
@@ -90,15 +474,23 @@ static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
}
|
||||
|
||||
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
const auto last_error = GetLastError();
|
||||
|
||||
// ignore signal if disabled or no exception info provided
|
||||
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) {
|
||||
if (!launcher::signal::DISABLE &&
|
||||
ExceptionInfo != nullptr &&
|
||||
ExceptionInfo->ExceptionRecord != nullptr &&
|
||||
InterlockedCompareExchange(&EXCEPTION_HANDLER_ACTIVE, 1, 0) == 0) {
|
||||
|
||||
// get exception record
|
||||
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
|
||||
|
||||
// print signal
|
||||
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
|
||||
log_exception_context(ExceptionInfo, last_error);
|
||||
|
||||
// write the minidump before other diagnostics inspect potentially damaged state
|
||||
write_minidump(ExceptionInfo);
|
||||
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
@@ -128,12 +520,11 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
// (e.g., ACIO init hang due to RTSS)
|
||||
deferredlogs::dump_to_logger(true);
|
||||
|
||||
// walk the exception chain
|
||||
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord;
|
||||
while (record_cause != nullptr) {
|
||||
log_warning("signal", "caused by: {}", exception_code(record_cause));
|
||||
record_cause = record_cause->ExceptionRecord;
|
||||
}
|
||||
// dump memory information before inspecting potentially damaged exception and stack data
|
||||
memutils::show_available_memory();
|
||||
|
||||
// walk the exception chain with bounded, checked reads
|
||||
log_exception_chain(ExceptionRecord);
|
||||
|
||||
// print stacktrace
|
||||
StackWalker sw;
|
||||
@@ -142,47 +533,12 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
log_warning("signal", "failed to print callstack");
|
||||
}
|
||||
|
||||
if (MiniDumpWriteDump_local != nullptr) {
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
CloseHandle(minidump_file);
|
||||
} else {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
}
|
||||
|
||||
// dump memory information
|
||||
memutils::show_available_memory();
|
||||
|
||||
// this will stall all UI threads for this process
|
||||
show_popup_for_crash();
|
||||
|
||||
log_fatal("signal", "end");
|
||||
|
||||
InterlockedExchange(&EXCEPTION_HANDLER_ACTIVE, 0);
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
+34
-32
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -382,52 +383,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 29;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 21;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 17;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 22;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 18;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 23;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 19;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 26;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
|
||||
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -455,52 +457,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 5;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 1;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 6;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 2;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 7;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 3;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 16;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 4;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "eamuse.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
@@ -10,7 +11,6 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
#include "overlay/overlay.h"
|
||||
@@ -335,7 +335,6 @@ void eamuse_coin_start_thread() {
|
||||
COIN_INPUT_THREAD = new std::thread([]() {
|
||||
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
static bool COIN_INPUT_KEY_STATE = false;
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
while (COIN_INPUT_THREAD_ACTIVE) {
|
||||
|
||||
// check input key
|
||||
@@ -355,7 +354,7 @@ void eamuse_coin_start_thread() {
|
||||
}
|
||||
|
||||
// once every two frames
|
||||
timer.sleep(1000 / 30);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -409,7 +408,6 @@ void eamuse_pin_macro_start_thread() {
|
||||
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
||||
|
||||
std::optional<uint8_t> active_unit = std::nullopt;
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
while (PIN_MACRO_THREAD_ACTIVE) {
|
||||
// wait for key press or auto-trigger
|
||||
@@ -438,7 +436,7 @@ void eamuse_pin_macro_start_thread() {
|
||||
}
|
||||
|
||||
if (!active_unit.has_value()) {
|
||||
timer.sleep(20);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -454,22 +452,22 @@ void eamuse_pin_macro_start_thread() {
|
||||
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
||||
}
|
||||
pin_index[unit]++;
|
||||
timer.sleep(100);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// clear
|
||||
eamuse_set_keypad_overrides(unit, 0);
|
||||
timer.sleep(50);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// end of PIN
|
||||
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
||||
active_unit = std::nullopt;
|
||||
timer.sleep(120);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
timer.sleep(200);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "wintouchemu.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#define TOUCH_SIMULATE_FAT_FINGERS 0
|
||||
#define TOUCH_DEBUG_VERBOSE 0
|
||||
|
||||
#if TOUCH_DEBUG_VERBOSE
|
||||
#define log_debug(module, format_str, ...) logger::push( \
|
||||
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
|
||||
#else
|
||||
#define log_debug(module, format_str, ...)
|
||||
#endif
|
||||
|
||||
namespace nativetouchhook {
|
||||
|
||||
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||
|
||||
static void strip_contact_size(PTOUCHINPUT point) {
|
||||
|
||||
#if TOUCH_SIMULATE_FAT_FINGERS
|
||||
point->dwMask |= 0x004;
|
||||
point->cxContact = 80 * 100;
|
||||
point->cyContact = 60 * 100;
|
||||
#endif
|
||||
|
||||
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
|
||||
// monitors that do set it, IIDX can get very confused (SDVX is not
|
||||
// affected)
|
||||
//
|
||||
// while the test menu and the touch "glow" seem to work properly,
|
||||
// interacting with subscreen menu items or entering PIN becomes
|
||||
// very unpredictable
|
||||
//
|
||||
// to fix this, simply remove the contact area width and height
|
||||
//
|
||||
// note: test menu > I/O > touch test gives 5 numbers:
|
||||
// n: x, y, w, h
|
||||
// where
|
||||
// n is the nth touch input since boot
|
||||
// x, y are coordinates (center of finger)
|
||||
// w, h are contact width and height
|
||||
//
|
||||
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
|
||||
// automatically be seen as 1x1, which works perfectly fine
|
||||
|
||||
log_debug(
|
||||
"touch::native",
|
||||
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
|
||||
point->x / 100,
|
||||
point->y / 100,
|
||||
point->dwMask,
|
||||
point->cxContact,
|
||||
point->cyContact);
|
||||
|
||||
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
|
||||
point->cxContact = 0;
|
||||
point->cyContact = 0;
|
||||
}
|
||||
|
||||
static void flip_touch_points(PTOUCHINPUT point) {
|
||||
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
||||
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
||||
}
|
||||
|
||||
static BOOL WINAPI GetTouchInputInfoHook(
|
||||
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||
|
||||
// call the original fist
|
||||
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
||||
if (result == 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool flip_values = false;
|
||||
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
|
||||
log_debug(
|
||||
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
|
||||
rawinput::touch::DISPLAY_ORIENTATION,
|
||||
rawinput::touch::DISPLAY_SIZE_X,
|
||||
rawinput::touch::DISPLAY_SIZE_Y);
|
||||
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
||||
flip_values = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawinput::touch::INVERTED) {
|
||||
flip_values = !flip_values;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cInputs; i++) {
|
||||
PTOUCHINPUT point = &pInputs[i];
|
||||
|
||||
if (avs::game::is_model("LDJ")) {
|
||||
strip_contact_size(point);
|
||||
}
|
||||
|
||||
if (flip_values) {
|
||||
flip_touch_points(point);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void hook(HMODULE module) {
|
||||
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
||||
if (GetTouchInputInfo_orig != nullptr) {
|
||||
log_misc("touch::native", "GetTouchInputInfo hooked");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouchhook {
|
||||
void hook(HMODULE module);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ static cardunit_card_cardnumber_t cardunit_card_cardnumber = nullptr;
|
||||
static HINSTANCE SCIUNIT_INSTANCE;
|
||||
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
|
||||
static bool SCIUNIT_INITIALIZED = false;
|
||||
static int SCIUNIT_STATUS = 0;
|
||||
static bool CARD_IN = false;
|
||||
static bool CARD_PRESSED = false;
|
||||
static uint8_t CARD_UID[8];
|
||||
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_errorunit() {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_stat() {
|
||||
return 0;
|
||||
return SCIUNIT_STATUS;
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_get_version(int a1, int a2) {
|
||||
@@ -163,6 +164,7 @@ static int __cdecl sciunit_get_version(int a1, int a2) {
|
||||
|
||||
static int __cdecl sciunit_initialize() {
|
||||
SCIUNIT_INITIALIZED = true;
|
||||
SCIUNIT_STATUS = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
|
||||
}
|
||||
|
||||
static int __cdecl sciunit_update() {
|
||||
if (SCIUNIT_INITIALIZED)
|
||||
if (SCIUNIT_INITIALIZED) {
|
||||
if (SCIUNIT_STATUS == 1) {
|
||||
SCIUNIT_STATUS = 2;
|
||||
}
|
||||
update_card();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -495,6 +495,14 @@ namespace wintouchemu {
|
||||
if (hWnd != nullptr && USE_MOUSE) {
|
||||
bool button_pressed = get_async_primary_mouse();
|
||||
|
||||
// while the subscreen overlay cannot accept the mouse, treat the button as
|
||||
// released; this ends any active touch and prevents new mouse touch contacts
|
||||
if (overlay::OVERLAY &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||
button_pressed = false;
|
||||
}
|
||||
|
||||
// if there was a touch event in the last 500 ms, don't insert new button presses
|
||||
if (button_pressed && (now - last_touch_event) < 500) {
|
||||
button_pressed = false;
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include "games/io.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/rb/touch_debug.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
@@ -50,6 +52,7 @@
|
||||
#include "windows/sdvx_sub.h"
|
||||
#include "windows/keypad.h"
|
||||
#include "windows/log.h"
|
||||
#include "windows/nostalgia_touch_piano.h"
|
||||
#include "windows/obs.h"
|
||||
#include "windows/patch_manager.h"
|
||||
#include "windows/exitprompt.cpp"
|
||||
@@ -66,6 +69,7 @@ namespace overlay {
|
||||
bool AUTO_SHOW_IOPANEL = false;
|
||||
bool AUTO_SHOW_KEYPAD_P1 = false;
|
||||
bool AUTO_SHOW_KEYPAD_P2 = false;
|
||||
bool ENABLE_KEYBOARD_NAVIGATION = false;
|
||||
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
|
||||
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
|
||||
std::optional<uint32_t> UI_SCALE_PERCENT;
|
||||
@@ -311,6 +315,9 @@ void overlay::SpiceOverlay::init() {
|
||||
auto &io = ImGui::GetIO();
|
||||
io.UserData = this;
|
||||
io.ConfigFlags = ImGuiConfigFlags_None;
|
||||
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
}
|
||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
}
|
||||
@@ -399,6 +406,12 @@ void overlay::SpiceOverlay::init() {
|
||||
window_fps->set_active(true);
|
||||
}
|
||||
|
||||
if (!cfg::CONFIGURATOR_STANDALONE &&
|
||||
avs::game::is_model("PAN") && games::nost::ENABLE_TOUCH_MODE) {
|
||||
window_nostalgia_touch_piano =
|
||||
std::make_unique<overlay::windows::NostalgiaTouchPiano>(this);
|
||||
}
|
||||
|
||||
// owned separately from `windows` so it is not part of the overlay window layer
|
||||
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
|
||||
|
||||
@@ -551,9 +564,18 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_fps->get_active();
|
||||
|
||||
const bool draw_nostalgia_touch_piano = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_nostalgia_touch_piano
|
||||
&& this->window_nostalgia_touch_piano->get_active();
|
||||
|
||||
// draw RB touch diagnostics
|
||||
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& games::rb::touch_debug_overlay_enabled();
|
||||
|
||||
// check if there is nothing to draw
|
||||
this->has_pending_frame = false;
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent) {
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent &&
|
||||
!draw_nostalgia_touch_piano && !draw_rb_touch_debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,12 +600,22 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
ImGui::NewFrame();
|
||||
this->has_pending_frame = true;
|
||||
|
||||
if (draw_rb_touch_debug) {
|
||||
games::rb::touch_draw_debug_overlay();
|
||||
}
|
||||
|
||||
// build windows only when the overlay itself is active
|
||||
if (this->active) {
|
||||
for (auto &window : this->windows) {
|
||||
window->build();
|
||||
}
|
||||
}
|
||||
|
||||
if (draw_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->build();
|
||||
}
|
||||
|
||||
if (this->active) {
|
||||
// draw the main menu on top of the overlay windows
|
||||
this->window_main_menu->build();
|
||||
|
||||
@@ -595,6 +627,7 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
if (draw_fps_persistent) {
|
||||
this->window_fps->build();
|
||||
}
|
||||
|
||||
// draw notifications last so they paint on top of any overlay windows
|
||||
if (draw_notifications) {
|
||||
overlay::notifications::draw();
|
||||
@@ -805,6 +838,10 @@ void overlay::SpiceOverlay::update() {
|
||||
// FPS window
|
||||
this->window_fps->update();
|
||||
|
||||
if (this->window_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->update();
|
||||
}
|
||||
|
||||
// main menu (owned separately from the overlay window layer)
|
||||
this->window_main_menu->update();
|
||||
|
||||
@@ -871,6 +908,17 @@ bool overlay::SpiceOverlay::get_active() {
|
||||
return this->active;
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::is_subscreen_overlay_visible() {
|
||||
return this->get_active() &&
|
||||
this->window_sub != nullptr &&
|
||||
this->window_sub->get_active();
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::accepts_subscreen_mouse_input() {
|
||||
return this->is_subscreen_overlay_visible() &&
|
||||
this->window_sub->is_mouse_hovered();
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::has_focus() {
|
||||
return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace overlay {
|
||||
extern bool AUTO_SHOW_IOPANEL;
|
||||
extern bool AUTO_SHOW_KEYPAD_P1;
|
||||
extern bool AUTO_SHOW_KEYPAD_P2;
|
||||
extern bool ENABLE_KEYBOARD_NAVIGATION;
|
||||
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
|
||||
extern FpsLocation FPS_LOCATION;
|
||||
extern bool SHOW_DEBUG_LOG_WINDOW;
|
||||
@@ -82,6 +83,7 @@ namespace overlay {
|
||||
// not part of `windows`: drawn/updated on the persistent layer (like
|
||||
// notifications), independent of the overlay's active state and input gates.
|
||||
std::unique_ptr<Window> window_fps;
|
||||
std::unique_ptr<Window> window_nostalgia_touch_piano;
|
||||
|
||||
// not part of `windows`: the main menu / launcher. owned and drawn
|
||||
// separately from the overlay window layer; it drives the overlay's
|
||||
@@ -107,6 +109,7 @@ namespace overlay {
|
||||
void show_main_menu();
|
||||
void set_active(bool active);
|
||||
bool get_active();
|
||||
bool accepts_subscreen_mouse_input();
|
||||
bool has_focus();
|
||||
bool hotkeys_triggered();
|
||||
|
||||
@@ -137,21 +140,24 @@ namespace overlay {
|
||||
inline IDirect3DDevice9 *get_device() {
|
||||
return this->device;
|
||||
}
|
||||
inline HWND get_window() {
|
||||
return this->hWnd;
|
||||
}
|
||||
|
||||
bool can_transform_touch_input() {
|
||||
return (this->subscreen_mouse_handler != nullptr);
|
||||
bool has_subscreen_touch_transform() {
|
||||
return (this->subscreen_touch_transform != nullptr);
|
||||
}
|
||||
|
||||
bool transform_touch_point(LONG *x, LONG *y) {
|
||||
if (this->get_active() && this->subscreen_mouse_handler) {
|
||||
return this->subscreen_mouse_handler(x, y);
|
||||
if (this->get_active() && this->subscreen_touch_transform) {
|
||||
return this->subscreen_touch_transform(x, y);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void set_subscreen_mouse_handler(const std::function<bool(LONG *, LONG *)> &f) {
|
||||
this->subscreen_mouse_handler = f;
|
||||
void set_subscreen_touch_transform(const std::function<bool(LONG *, LONG *)> &f) {
|
||||
this->subscreen_touch_transform = f;
|
||||
}
|
||||
|
||||
// renderer
|
||||
@@ -198,7 +204,7 @@ namespace overlay {
|
||||
|
||||
std::vector<std::unique_ptr<Window>> windows;
|
||||
|
||||
std::function<bool(LONG *, LONG *)> subscreen_mouse_handler = nullptr;
|
||||
std::function<bool(LONG *, LONG *)> subscreen_touch_transform = nullptr;
|
||||
|
||||
bool active = false;
|
||||
bool toggle_down = false;
|
||||
@@ -211,6 +217,7 @@ namespace overlay {
|
||||
// so render() never runs without a matching NewFrame.
|
||||
bool has_pending_frame = false;
|
||||
|
||||
bool is_subscreen_overlay_visible();
|
||||
void init();
|
||||
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ void overlay::Window::build() {
|
||||
|
||||
// check if active
|
||||
if (!this->active) {
|
||||
this->mouse_hovered = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,10 +107,18 @@ void overlay::Window::build() {
|
||||
// top spot. suppress that so only an explicit focus request (see below)
|
||||
// decides what comes to the front.
|
||||
const std::string window_id = this->title + "###" + to_string(this);
|
||||
if (ImGui::Begin(
|
||||
const bool build_contents = ImGui::Begin(
|
||||
window_id.c_str(),
|
||||
&this->active,
|
||||
this->flags | ImGuiWindowFlags_NoFocusOnAppearing)) {
|
||||
this->flags | ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
|
||||
// keep the window hovered while its invisible input button is held during a drag
|
||||
this->mouse_hovered =
|
||||
ImGui::IsWindowHovered(
|
||||
ImGuiHoveredFlags_RootAndChildWindows |
|
||||
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||
|
||||
if (build_contents) {
|
||||
|
||||
// window attributes - init_pos / init_size are only honored once
|
||||
// (ImGuiCond_Once), so compute them a single time instead of every frame
|
||||
@@ -154,6 +163,9 @@ void overlay::Window::build() {
|
||||
|
||||
} else {
|
||||
|
||||
// raw content does not have an ImGui window to hover
|
||||
this->mouse_hovered = false;
|
||||
|
||||
// add raw content
|
||||
this->build_content();
|
||||
}
|
||||
@@ -201,3 +213,7 @@ bool overlay::Window::get_active() {
|
||||
// now it depends on us
|
||||
return this->active;
|
||||
}
|
||||
|
||||
bool overlay::Window::is_mouse_hovered() {
|
||||
return this->mouse_hovered;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace overlay {
|
||||
void toggle_active();
|
||||
void set_active(bool active);
|
||||
bool get_active();
|
||||
bool is_mouse_hovered();
|
||||
|
||||
// raise this window to the top of the z-order on its next build. only
|
||||
// call in response to an explicit user action (keyboard toggle, UI
|
||||
@@ -37,6 +38,7 @@ namespace overlay {
|
||||
// set when the window transitions to visible so build() raises it to the
|
||||
// top of the z-order on the next frame
|
||||
bool request_focus = false;
|
||||
bool mouse_hovered = false;
|
||||
std::vector<Window*> children;
|
||||
|
||||
// settings
|
||||
|
||||
@@ -82,9 +82,14 @@ namespace overlay::windows {
|
||||
ImGui::Text("Rendering");
|
||||
|
||||
// Media Type Selector
|
||||
int selectedMediaTypeIndex = selectedCamera->m_selectedMediaTypeIndex;
|
||||
int selectedMediaTypeIndex = selectedCamera->GetSelectedMediaTypeIndex();
|
||||
auto numMediaTypes = selectedCamera->m_mediaTypeInfos.size();
|
||||
|
||||
if (numMediaTypes == 0) {
|
||||
ImGui::TextColored(ImVec4(1.f, 1.f, 0.f, 1.f), "%s", "No supported media types");
|
||||
return;
|
||||
}
|
||||
|
||||
auto selectedMediaTypeIndexChanged = ImGui::BeginCombo(
|
||||
"Media Type", selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).description.c_str()
|
||||
);
|
||||
@@ -103,9 +108,9 @@ namespace overlay::windows {
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->m_selectedMediaTypeIndex) {
|
||||
if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->GetSelectedMediaTypeIndex()) {
|
||||
selectedCamera->m_useAutoMediaType = false;
|
||||
selectedCamera->ChangeMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType);
|
||||
selectedCamera->RequestMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType);
|
||||
}
|
||||
|
||||
// Auto media type
|
||||
@@ -114,13 +119,14 @@ namespace overlay::windows {
|
||||
if (ImGui::Checkbox("Auto##MediaType", &isAutoMediaType)) {
|
||||
selectedCamera->m_useAutoMediaType = isAutoMediaType;
|
||||
if (isAutoMediaType) {
|
||||
selectedCamera->ChangeMediaType(selectedCamera->m_pAutoMediaType);
|
||||
selectedCamera->RequestMediaType(selectedCamera->m_pAutoMediaType);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw mode
|
||||
int selectedDrawModeIndex = selectedCamera->m_drawMode;
|
||||
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[selectedCamera->m_drawMode].c_str())) {
|
||||
const auto drawMode = selectedCamera->m_drawMode.load();
|
||||
int selectedDrawModeIndex = drawMode;
|
||||
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[drawMode].c_str())) {
|
||||
for (size_t i = 0; i < DRAW_MODE_SIZE; i++) {
|
||||
const bool is_selected = (selectedDrawModeIndex == (int) i);
|
||||
if (ImGui::Selectable(DRAW_MODE_LABELS[i].c_str(), is_selected)) {
|
||||
@@ -142,15 +148,25 @@ namespace overlay::windows {
|
||||
"Letterbox to 4:3: Like Letterbox, but target 4:3"
|
||||
);
|
||||
|
||||
if (selectedDrawModeIndex != selectedCamera->m_drawMode) {
|
||||
selectedCamera->m_drawMode = (LocalCameraDrawMode)selectedDrawModeIndex;
|
||||
if (selectedDrawModeIndex != selectedCamera->m_drawMode.load()) {
|
||||
selectedCamera->m_drawMode.store((LocalCameraDrawMode)selectedDrawModeIndex);
|
||||
selectedCamera->UpdateDrawRect();
|
||||
}
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Checkbox("Horizontal Flip", &selectedCamera->m_flipHorizontal);
|
||||
bool flipHorizontal = selectedCamera->m_flipHorizontal.load();
|
||||
if (ImGui::Checkbox("Horizontal Flip", &flipHorizontal)) {
|
||||
selectedCamera->m_flipHorizontal.store(flipHorizontal);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Vertical Flip", &selectedCamera->m_flipVertical);
|
||||
bool flipVertical = selectedCamera->m_flipVertical.load();
|
||||
if (ImGui::Checkbox("Vertical Flip", &flipVertical)) {
|
||||
selectedCamera->m_flipVertical.store(flipVertical);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(
|
||||
"Flips are done by the CPU and can be very expensive, potentially causing frame drops! "
|
||||
"Prefer to use camera's built-in flip if available.");
|
||||
|
||||
// Camera control parameters
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace overlay::windows {
|
||||
static const std::vector<std::pair<const char *, ControllerPage>> CONTROLLER_PAGE_GROUPS = {
|
||||
{ "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS },
|
||||
{ "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS },
|
||||
{ "Modifiers", ControllerPage::CONTROLLER_PAGE_MODIFIERS },
|
||||
{ "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS },
|
||||
{ "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY },
|
||||
{ "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS },
|
||||
@@ -251,7 +252,7 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
// Renders an arrow-less, non-collapsible left-nav header row, highlighted when
|
||||
// active. Returns true if the row was clicked this frame; callers apply their
|
||||
// active. Returns true if the row was activated this frame; callers apply their
|
||||
// own selection side effects.
|
||||
bool Config::build_nav_header(const char *label, bool active) {
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf;
|
||||
@@ -282,7 +283,7 @@ namespace overlay::windows {
|
||||
ImGui::PopStyleColor(colors_pushed);
|
||||
}
|
||||
|
||||
return ImGui::IsItemClicked();
|
||||
return ImGui::IsItemActivated();
|
||||
}
|
||||
|
||||
void Config::build_options_tab(float page_offset) {
|
||||
@@ -616,6 +617,21 @@ namespace overlay::windows {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ControllerPage::CONTROLLER_PAGE_MODIFIERS: {
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Modifiers");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Bind modifier buttons below, then open Edit properties on any button binding "
|
||||
"(except MIDI) to select the required modifiers. This allows you to configure "
|
||||
"button combinations.\n\n"
|
||||
"For example, if you bind the Start button as Modifier 1, and then bind "
|
||||
"the A button for Service Coin and set it to require Modifier 1, you press "
|
||||
"Start + A to trigger Service Coin.");
|
||||
ImGui::TextUnformatted("");
|
||||
this->build_buttons(
|
||||
"Modifier", games::get_buttons_modifiers(this->games_selected_name));
|
||||
break;
|
||||
}
|
||||
case ControllerPage::CONTROLLER_PAGE_ANALOGS: {
|
||||
|
||||
// help text for binding analog, if the game has one
|
||||
@@ -1256,7 +1272,8 @@ namespace overlay::windows {
|
||||
if (open_edit) {
|
||||
ImGui::OpenPopup(edit_name.c_str());
|
||||
}
|
||||
edit_button_popup(edit_name, button_display, button, button_velocity, alt_index);
|
||||
edit_button_popup(
|
||||
edit_name, button_display, button, button_velocity, alt_index, name != "Modifier");
|
||||
|
||||
// clean up
|
||||
ImGui::PopID();
|
||||
@@ -1275,6 +1292,7 @@ namespace overlay::windows {
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setInvert(false);
|
||||
button->setModifierMask(0);
|
||||
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
|
||||
button->setLastVelocity(0);
|
||||
button->setTemporary(false);
|
||||
@@ -1356,8 +1374,9 @@ namespace overlay::windows {
|
||||
if (check_devices) {
|
||||
// iterate updated devices
|
||||
auto updated_devices = RI_MGR->devices_get_updated();
|
||||
bool binding_finished = false;
|
||||
for (auto device : updated_devices) {
|
||||
std::lock_guard<std::mutex> lock(*device->mutex);
|
||||
std::unique_lock<std::mutex> lock(*device->mutex);
|
||||
switch (device->type) {
|
||||
case rawinput::MOUSE: {
|
||||
auto mouse = device->mouseInfo;
|
||||
@@ -1369,12 +1388,13 @@ namespace overlay::windows {
|
||||
button->setDeviceIdentifier(device->name);
|
||||
button->setVKey(static_cast<unsigned short>(i));
|
||||
button->setAnalogType(BAT_NONE);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1393,7 +1413,7 @@ namespace overlay::windows {
|
||||
buttons_bind_active = false;
|
||||
buttons_many_index = -1;
|
||||
buttons_many_active = false;
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1401,12 +1421,13 @@ namespace overlay::windows {
|
||||
button->setDeviceIdentifier(device->name);
|
||||
button->setVKey(vkey);
|
||||
button->setAnalogType(BAT_NONE);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1536,13 +1557,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1581,6 +1603,7 @@ namespace overlay::windows {
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
// same idea as setMidiVKey - keep velocity threshold consistent
|
||||
button->setVelocityThreshold(
|
||||
device->midiInfo->v2_velocity_threshold[button->getVKey()]);
|
||||
@@ -1590,7 +1613,7 @@ namespace overlay::windows {
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1608,13 +1631,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1635,13 +1659,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1662,13 +1687,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1688,13 +1714,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1709,13 +1736,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1739,13 +1767,14 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
ImGui::CloseCurrentPopup();
|
||||
buttons_bind_active = false;
|
||||
inc_buttons_many_index(button_it_max);
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
binding_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1755,6 +1784,13 @@ namespace overlay::windows {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lock.unlock();
|
||||
if (binding_finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (binding_finished) {
|
||||
RI_MGR->devices_midi_freeze(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1808,6 +1844,7 @@ namespace overlay::windows {
|
||||
if (RI_MGR->XINPUT_MGR->get_any_button_pressed(xinput)) {
|
||||
button->setDeviceIdentifier(xinput::get_device_desc(xinput.player));
|
||||
button->setVKey(static_cast<uint16_t>(xinput.button));
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button, alt_index - 1);
|
||||
inc_buttons_many_index(button_it_max);
|
||||
@@ -1864,6 +1901,7 @@ namespace overlay::windows {
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setModifierMask(0);
|
||||
::Config::getInstance().updateBinding(
|
||||
games_list[games_selected], *button,
|
||||
alt_index - 1);
|
||||
@@ -1882,27 +1920,103 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::build_modifier_picker(Button &button) {
|
||||
bool changed = false;
|
||||
auto modifier_mask = button.getModifierMask();
|
||||
|
||||
// collect live states and build the closed dropdown summary
|
||||
uint8_t pressed_modifier_mask = 0;
|
||||
std::string modifier_preview;
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
const uint8_t modifier_bit = UINT8_C(1) << index;
|
||||
if (modifier_buttons && index < modifier_buttons->size() &&
|
||||
GameAPI::Buttons::getState(RI_MGR, modifier_buttons->at(index))) {
|
||||
pressed_modifier_mask |= modifier_bit;
|
||||
}
|
||||
if ((modifier_mask & modifier_bit) != 0) {
|
||||
modifier_preview += modifier_preview.empty() ? "Mod " : "+";
|
||||
modifier_preview += std::to_string(index + 1);
|
||||
}
|
||||
}
|
||||
if (modifier_preview.empty()) {
|
||||
modifier_preview = "None";
|
||||
}
|
||||
|
||||
// color the combo when all requirements are held, but keep its label unchanged
|
||||
const bool all_modifiers_pressed = modifier_mask != 0 &&
|
||||
(pressed_modifier_mask & modifier_mask) == modifier_mask;
|
||||
if (all_modifiers_pressed) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
const bool combo_open = ImGui::BeginCombo("##Modifiers", modifier_preview.c_str());
|
||||
if (all_modifiers_pressed) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// update requirements while highlighting modifier sources that are currently pressed
|
||||
if (combo_open) {
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
const uint8_t modifier_bit = UINT8_C(1) << index;
|
||||
bool required = (modifier_mask & modifier_bit) != 0;
|
||||
const auto label = fmt::format("Modifier {}", index + 1);
|
||||
const bool modifier_pressed = (pressed_modifier_mask & modifier_bit) != 0;
|
||||
if (modifier_pressed) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
const bool requirement_changed = ImGui::Checkbox(label.c_str(), &required);
|
||||
if (modifier_pressed) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
if (requirement_changed) {
|
||||
modifier_mask ^= modifier_bit;
|
||||
button.setModifierMask(modifier_mask);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
|
||||
ImGui::TextUnformatted("Modifiers");
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void Config::edit_button_popup(
|
||||
const std::string &edit_name,
|
||||
const std::string &button_display,
|
||||
Button *button,
|
||||
const float button_velocity,
|
||||
const int alt_index) {
|
||||
const int alt_index,
|
||||
const bool allow_modifiers) {
|
||||
|
||||
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
|
||||
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
bool dirty = false;
|
||||
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
|
||||
|
||||
auto switch_device = [&](const std::string &device_identifier) {
|
||||
button->setDeviceIdentifier(device_identifier);
|
||||
button->setAnalogType(ButtonAnalogType::BAT_NONE);
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setVKey(0);
|
||||
button->setInvert(false);
|
||||
button->setModifierMask(0);
|
||||
dirty = true;
|
||||
};
|
||||
|
||||
// binding
|
||||
ImGui::Text("Binding");
|
||||
ImGui::Separator();
|
||||
|
||||
// combo for devices
|
||||
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
|
||||
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
|
||||
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) {
|
||||
button->setDeviceIdentifier("");
|
||||
dirty = true;
|
||||
if (ImGui::Selectable("Empty (Naive)", button->isNaive()) && !button->isNaive()) {
|
||||
switch_device("");
|
||||
}
|
||||
if (button->isNaive()) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -1910,17 +2024,8 @@ namespace overlay::windows {
|
||||
for (auto &device : RI_MGR->devices_get()) {
|
||||
bool selected = button->getDeviceIdentifier() == device.name.c_str();
|
||||
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
|
||||
if (ImGui::Selectable(device_desc.c_str(), selected)) {
|
||||
button->setDeviceIdentifier(device.name);
|
||||
// reset controls when switching devices
|
||||
button->setAnalogType(ButtonAnalogType::BAT_NONE);
|
||||
button->setDebounceUp(0.0);
|
||||
button->setDebounceDown(0.0);
|
||||
button->setBatThreshold(0);
|
||||
button->setVelocityThreshold(0);
|
||||
button->setVKey(0);
|
||||
button->setInvert(false);
|
||||
dirty = true;
|
||||
if (ImGui::Selectable(device_desc.c_str(), selected) && !selected) {
|
||||
switch_device(device.name);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -1928,6 +2033,7 @@ namespace overlay::windows {
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
device = RI_MGR->devices_get(button->getDeviceIdentifier());
|
||||
|
||||
// analog type (only for HID)
|
||||
const auto bat = button->getAnalogType();
|
||||
@@ -2219,21 +2325,22 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifiers
|
||||
{
|
||||
const bool is_midi = device != nullptr && device->type == rawinput::MIDI;
|
||||
if (allow_modifiers && button->isValid() && !is_midi) {
|
||||
dirty |= build_modifier_picker(*button);
|
||||
}
|
||||
}
|
||||
|
||||
// preview
|
||||
if (!button_display.empty()) {
|
||||
ImGui::TextUnformatted("\nPreview");
|
||||
ImGui::Separator();
|
||||
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
|
||||
}
|
||||
// this doesn't account for utf-8 but whatever
|
||||
if (button_display.size() >= 40) {
|
||||
ImGui::Text("%.37s...", button_display.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(button_display.c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(button_display.c_str());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
ImGui::TextTruncated(button_display, ImGui::GetContentRegionAvail().x);
|
||||
ImGui::TextUnformatted("\n");
|
||||
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2242,8 +2349,10 @@ namespace overlay::windows {
|
||||
ImGui::TextUnformatted("");
|
||||
}
|
||||
|
||||
|
||||
// options
|
||||
ImGui::Text("Options");
|
||||
ImGui::TextUnformatted("Options");
|
||||
ImGui::Separator();
|
||||
|
||||
// check for debounce
|
||||
if (button->getDebounceUp() || button->getDebounceDown()
|
||||
@@ -2362,6 +2471,7 @@ namespace overlay::windows {
|
||||
ImGui::ProgressBar(button_velocity);
|
||||
} else {
|
||||
ImGui::Text("State");
|
||||
ImGui::Separator();
|
||||
ImGui::ProgressBar(button_velocity);
|
||||
}
|
||||
|
||||
@@ -5482,6 +5592,12 @@ namespace overlay::windows {
|
||||
tmpl.buttons.emplace_back(btn);
|
||||
}
|
||||
}
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
if (modifier_buttons) {
|
||||
for (auto &modifier : *modifier_buttons) {
|
||||
tmpl.modifier_buttons.emplace_back(modifier);
|
||||
}
|
||||
}
|
||||
|
||||
// capture keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
@@ -5537,6 +5653,17 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// clear all modifier buttons
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(this->games_selected_name);
|
||||
if (modifier_buttons) {
|
||||
for (auto &modifier : *modifier_buttons) {
|
||||
for (int ai = (int)modifier.getAlternatives().size() - 1; ai >= 0; ai--) {
|
||||
clear_button(&modifier.getAlternatives()[ai], ai + 1);
|
||||
}
|
||||
clear_button(&modifier, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// clear all keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
if (keypad_buttons) {
|
||||
@@ -5630,6 +5757,7 @@ namespace overlay::windows {
|
||||
btn.setDebounceDown(entry->debounce_down);
|
||||
btn.setBatThreshold(entry->bat_threshold);
|
||||
btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
btn.setModifierMask(entry->modifier_mask);
|
||||
::Config::getInstance().updateBinding(game, btn, -1);
|
||||
} else {
|
||||
Button alt_btn(btn.getName());
|
||||
@@ -5641,6 +5769,7 @@ namespace overlay::windows {
|
||||
alt_btn.setDebounceDown(entry->debounce_down);
|
||||
alt_btn.setBatThreshold(entry->bat_threshold);
|
||||
alt_btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
alt_btn.setModifierMask(entry->modifier_mask);
|
||||
alt_btn.setTemporary(true);
|
||||
btn.getAlternatives().push_back(alt_btn);
|
||||
::Config::getInstance().updateBinding(
|
||||
@@ -5659,6 +5788,11 @@ namespace overlay::windows {
|
||||
if (this->apply_buttons) {
|
||||
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name));
|
||||
}
|
||||
if (this->apply_modifiers) {
|
||||
apply_buttons(
|
||||
tmpl.modifier_buttons,
|
||||
games::get_buttons_modifiers(this->games_selected_name));
|
||||
}
|
||||
if (this->apply_keypads) {
|
||||
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
|
||||
}
|
||||
@@ -5900,7 +6034,7 @@ namespace overlay::windows {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(
|
||||
"Clears all game button, analog, and light bindings.");
|
||||
"Clears all game button, modifier, keypad, analog, and light bindings.");
|
||||
if (this->all_cleared) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Done.");
|
||||
@@ -5915,6 +6049,8 @@ namespace overlay::windows {
|
||||
bool selection_changed = false;
|
||||
selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Modifiers", &this->apply_modifiers);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads);
|
||||
ImGui::SameLine();
|
||||
selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs);
|
||||
@@ -5924,7 +6060,7 @@ namespace overlay::windows {
|
||||
std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false);
|
||||
}
|
||||
|
||||
if (!this->apply_buttons && !this->apply_keypads &&
|
||||
if (!this->apply_buttons && !this->apply_modifiers && !this->apply_keypads &&
|
||||
!this->apply_analogs && !this->apply_lights) {
|
||||
ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n");
|
||||
}
|
||||
@@ -5964,7 +6100,8 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
|
||||
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
|
||||
!apply_analogs && !apply_lights) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
@@ -6133,7 +6270,8 @@ namespace overlay::windows {
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) {
|
||||
if (!apply_buttons && !apply_modifiers && !apply_keypads &&
|
||||
!apply_analogs && !apply_lights) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
@@ -6212,12 +6350,14 @@ namespace overlay::windows {
|
||||
ImGui::TextUnformatted("Pick which groups to save:");
|
||||
ImGui::Checkbox("Buttons", &this->save_buttons);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Modifiers", &this->save_modifiers);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Keypads", &this->save_keypads);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Analogs", &this->save_analogs);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Lights", &this->save_lights);
|
||||
if (!this->save_analogs && !this->save_keypads &&
|
||||
if (!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
|
||||
!this->save_analogs && !this->save_lights) {
|
||||
ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n");
|
||||
}
|
||||
@@ -6269,7 +6409,8 @@ namespace overlay::windows {
|
||||
|
||||
ImGui::BeginDisabled(
|
||||
!all_labels_set ||
|
||||
(!this->save_buttons && !this->save_keypads && !this->save_analogs && !this->save_lights));
|
||||
(!this->save_buttons && !this->save_modifiers && !this->save_keypads &&
|
||||
!this->save_analogs && !this->save_lights));
|
||||
|
||||
if (ImGui::Button("Save")) {
|
||||
// replace device IDs with labels in the template
|
||||
@@ -6278,7 +6419,8 @@ namespace overlay::windows {
|
||||
template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
if (save_user_template(template_pending_save,
|
||||
this->save_buttons, this->save_keypads, this->save_analogs, this->save_lights)) {
|
||||
this->save_buttons, this->save_modifiers, this->save_keypads,
|
||||
this->save_analogs, this->save_lights)) {
|
||||
template_save_name[0] = '\0';
|
||||
templates_cache_dirty = true;
|
||||
}
|
||||
@@ -6360,7 +6502,7 @@ namespace overlay::windows {
|
||||
t.rename_source(template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
}
|
||||
save_user_template(t, true, true, true, true);
|
||||
save_user_template(t, true, true, true, true, true);
|
||||
templates_cache_dirty = true;
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
CONTROLLER_PAGE_INVALID,
|
||||
CONTROLLER_PAGE_BUTTONS,
|
||||
CONTROLLER_PAGE_KEYPADS,
|
||||
CONTROLLER_PAGE_MODIFIERS,
|
||||
CONTROLLER_PAGE_ANALOGS,
|
||||
CONTROLLER_PAGE_OVERLAY,
|
||||
CONTROLLER_PAGE_LIGHTS,
|
||||
@@ -119,10 +120,12 @@ namespace overlay::windows {
|
||||
std::vector<std::string> template_save_sources;
|
||||
std::vector<std::string> template_save_labels;
|
||||
bool apply_buttons = true;
|
||||
bool apply_modifiers = true;
|
||||
bool apply_keypads = true;
|
||||
bool apply_analogs = true;
|
||||
bool apply_lights = true;
|
||||
bool save_buttons = true;
|
||||
bool save_modifiers = true;
|
||||
bool save_keypads = true;
|
||||
bool save_analogs = true;
|
||||
bool save_lights = true;
|
||||
@@ -168,12 +171,14 @@ namespace overlay::windows {
|
||||
|
||||
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max, const int alt_index);
|
||||
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max, const int alt_index);
|
||||
bool build_modifier_picker(Button &button);
|
||||
void edit_button_popup(
|
||||
const std::string &edit_name,
|
||||
const std::string &button_display,
|
||||
Button *button,
|
||||
const float button_velocity,
|
||||
const int alt_index);
|
||||
const int alt_index,
|
||||
const bool allow_modifiers);
|
||||
void clear_button(Button *button, const int alt_index, std::optional<unsigned short> vKey_default = std::nullopt);
|
||||
void reset_button_to_default(Button *button, unsigned short vKey_default);
|
||||
unsigned int get_keypad_top_row(const Button &button);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// helpers - iterate both buttons and keypad_buttons
|
||||
// helpers - iterate all button groups
|
||||
static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.is_naive()) naive++;
|
||||
@@ -21,6 +21,7 @@ namespace overlay::windows {
|
||||
int ControllerTemplate::count_naive_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(modifier_buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return naive;
|
||||
}
|
||||
@@ -28,6 +29,7 @@ namespace overlay::windows {
|
||||
int ControllerTemplate::count_device_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(modifier_buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return device;
|
||||
}
|
||||
@@ -64,6 +66,7 @@ namespace overlay::windows {
|
||||
std::set<std::string> ControllerTemplate::get_used_devices() const {
|
||||
std::set<std::string> devices;
|
||||
collect_devices_from(buttons, devices);
|
||||
collect_devices_from(modifier_buttons, devices);
|
||||
collect_devices_from(keypad_buttons, devices);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) devices.insert(a.device_identifier);
|
||||
@@ -94,6 +97,7 @@ namespace overlay::windows {
|
||||
bool has_naive = false;
|
||||
|
||||
collect_sources_from(buttons, sources_set, has_naive);
|
||||
collect_sources_from(modifier_buttons, sources_set, has_naive);
|
||||
collect_sources_from(keypad_buttons, sources_set, has_naive);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) {
|
||||
@@ -131,6 +135,7 @@ namespace overlay::windows {
|
||||
}
|
||||
};
|
||||
rename_in_buttons(buttons);
|
||||
rename_in_buttons(modifier_buttons);
|
||||
rename_in_buttons(keypad_buttons);
|
||||
for (auto &a : analogs) {
|
||||
if (a.device_identifier == old_id) a.device_identifier = new_id;
|
||||
@@ -181,6 +186,16 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
std::vector<std::string> modifier_lines;
|
||||
collect_btn_lines(modifier_buttons, modifier_lines);
|
||||
if (!modifier_lines.empty()) {
|
||||
result += "Modifiers:\n";
|
||||
for (auto &line : modifier_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
std::vector<std::string> kp_lines;
|
||||
collect_btn_lines(keypad_buttons, kp_lines);
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace overlay::windows {
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0;
|
||||
unsigned short velocity_threshold = 0;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
|
||||
bool is_device() const { return !device_identifier.empty(); }
|
||||
@@ -34,6 +35,7 @@ namespace overlay::windows {
|
||||
e.debounce_down = btn.getDebounceDown();
|
||||
e.bat_threshold = btn.getBatThreshold();
|
||||
e.velocity_threshold = btn.getVelocityThreshold();
|
||||
e.modifier_mask = btn.getModifierMask();
|
||||
return e;
|
||||
}
|
||||
};
|
||||
@@ -123,6 +125,7 @@ namespace overlay::windows {
|
||||
std::string game_name;
|
||||
bool is_builtin = false;
|
||||
std::vector<TemplateButtonBinding> buttons;
|
||||
std::vector<TemplateButtonBinding> modifier_buttons;
|
||||
std::vector<TemplateButtonBinding> keypad_buttons;
|
||||
std::vector<TemplateAnalogBinding> analogs;
|
||||
std::vector<TemplateLightBinding> lights;
|
||||
@@ -150,6 +153,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights);
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace overlay::windows {
|
||||
this->texture_width = 0;
|
||||
this->texture_height = 0;
|
||||
|
||||
overlay->set_subscreen_mouse_handler([this](LONG *x, LONG *y) -> bool {
|
||||
overlay->set_subscreen_touch_transform([this](LONG *x, LONG *y) -> bool {
|
||||
// convert to normalized form (relative window coordinates 0.f-1.f)
|
||||
ImVec2 xy;
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "nostalgia_touch_piano.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "external/imgui/imgui_internal.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
static constexpr float BUTTON_WIDTH = 144.f;
|
||||
static constexpr float BUTTON_HEIGHT = 40.f;
|
||||
static constexpr float WINDOW_PADDING = 4.f;
|
||||
static constexpr float EDGE_MARGIN = 4.f;
|
||||
static constexpr float PIANO_HEIGHT_RATIO = 0.08f;
|
||||
static constexpr float PIANO_LEFT_GAP = 11.f;
|
||||
static constexpr float PIANO_RIGHT_GAP = 10.f;
|
||||
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||
|
||||
static constexpr ImU32 PIANO_KEY_COLOR = IM_COL32(255, 255, 255, 50);
|
||||
static constexpr ImU32 PIANO_KEY_ACTIVE_COLOR = IM_COL32(255, 48, 48, 160);
|
||||
static constexpr ImU32 PIANO_KEY_BORDER_COLOR = IM_COL32(0, 0, 0, 100);
|
||||
|
||||
struct ButtonPalette {
|
||||
ImVec4 normal;
|
||||
ImVec4 hovered;
|
||||
ImVec4 active;
|
||||
};
|
||||
|
||||
static const ButtonPalette NAV_MODE_PALETTE {
|
||||
ImVec4(0.10f, 0.45f, 0.28f, 0.72f),
|
||||
ImVec4(0.14f, 0.58f, 0.36f, 0.82f),
|
||||
ImVec4(0.08f, 0.34f, 0.21f, 0.90f),
|
||||
};
|
||||
static const ButtonPalette PIANO_MODE_PALETTE {
|
||||
ImVec4(0.15f, 0.32f, 0.62f, 0.72f),
|
||||
ImVec4(0.20f, 0.42f, 0.78f, 0.82f),
|
||||
ImVec4(0.10f, 0.24f, 0.50f, 0.90f),
|
||||
};
|
||||
|
||||
static void draw_piano_keys(
|
||||
const ImVec2 &display_size,
|
||||
LONG client_width,
|
||||
uint32_t key_state) {
|
||||
|
||||
if (display_size.x <= 0.f || display_size.y <= 0.f || client_width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is only a visual guide; native touch routing owns the actual input
|
||||
const float left_gap = PIANO_LEFT_GAP * display_size.x / client_width;
|
||||
const float right_gap = PIANO_RIGHT_GAP * display_size.x / client_width;
|
||||
const float piano_width = display_size.x - left_gap - right_gap;
|
||||
if (piano_width <= 0.f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float key_width = piano_width / PIANO_KEY_COUNT;
|
||||
const float key_top = display_size.y * (1.f - PIANO_HEIGHT_RATIO);
|
||||
auto *draw_list = ImGui::GetBackgroundDrawList();
|
||||
|
||||
for (uint32_t key = 0; key < PIANO_KEY_COUNT; key++) {
|
||||
const ImVec2 key_min(left_gap + key * key_width, key_top);
|
||||
const ImVec2 key_max(left_gap + (key + 1) * key_width, display_size.y);
|
||||
const bool active = (key_state & (UINT32_C(1) << key)) != 0;
|
||||
draw_list->AddRectFilled(
|
||||
key_min,
|
||||
key_max,
|
||||
active ? PIANO_KEY_ACTIVE_COLOR : PIANO_KEY_COLOR);
|
||||
draw_list->AddRect(key_min, key_max, PIANO_KEY_BORDER_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
NostalgiaTouchPiano::NostalgiaTouchPiano(SpiceOverlay *overlay) : Window(overlay) {
|
||||
this->title = "Nostalgia Touch Piano";
|
||||
this->flags = ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoCollapse
|
||||
| ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoDocking
|
||||
| ImGuiWindowFlags_NoBackground
|
||||
| ImGuiWindowFlags_NoSavedSettings
|
||||
| ImGuiWindowFlags_NoNav
|
||||
| ImGuiWindowFlags_NoBringToFrontOnFocus;
|
||||
this->window_padding = overlay::apply_scaling_to_vector(WINDOW_PADDING, WINDOW_PADDING);
|
||||
this->set_active(true);
|
||||
}
|
||||
|
||||
void NostalgiaTouchPiano::calculate_initial_window() {
|
||||
this->init_size = overlay::apply_scaling_to_vector(
|
||||
BUTTON_WIDTH + WINDOW_PADDING * 2,
|
||||
BUTTON_HEIGHT + WINDOW_PADDING * 2);
|
||||
this->init_pos = overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN);
|
||||
}
|
||||
|
||||
void NostalgiaTouchPiano::build_content() {
|
||||
// keep the control anchored while the game window changes size or mode
|
||||
ImGui::SetWindowPos(
|
||||
overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN),
|
||||
ImGuiCond_Always);
|
||||
|
||||
// stay above regular overlay windows, but never cover a blocking modal
|
||||
ImGuiWindow *mode_window = ImGui::GetCurrentWindow();
|
||||
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
|
||||
ImGui::BringWindowToDisplayBehind(mode_window, modal);
|
||||
} else {
|
||||
ImGui::BringWindowToDisplayFront(mode_window);
|
||||
}
|
||||
|
||||
const bool nav_mode =
|
||||
games::nost::touch_mode::current_mode() == games::nost::touch_mode::Mode::Nav;
|
||||
const char *label = nav_mode ? "Nav Mode" : "Piano Mode";
|
||||
|
||||
// make the active routing mode recognizable without reading the label
|
||||
const auto &palette = nav_mode ? NAV_MODE_PALETTE : PIANO_MODE_PALETTE;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, palette.normal);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette.hovered);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette.active);
|
||||
ImGui::Button(label, overlay::apply_scaling_to_vector(BUTTON_WIDTH, BUTTON_HEIGHT));
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
const auto &io = ImGui::GetIO();
|
||||
RECT client_rect {};
|
||||
if (io.DisplaySize.x > 0.f && io.DisplaySize.y > 0.f &&
|
||||
GetClientRect(this->overlay->get_window(), &client_rect)) {
|
||||
|
||||
// convert the rendered imgui rectangle into the client coordinates
|
||||
// used by hardware touch publication and piano-key mapping
|
||||
const auto item_min = ImGui::GetItemRectMin();
|
||||
const auto item_max = ImGui::GetItemRectMax();
|
||||
const auto client_width = client_rect.right - client_rect.left;
|
||||
const auto client_height = client_rect.bottom - client_rect.top;
|
||||
if (!nav_mode) {
|
||||
draw_piano_keys(
|
||||
io.DisplaySize,
|
||||
client_width,
|
||||
games::nost::touch_mode::piano_key_state());
|
||||
}
|
||||
|
||||
RECT button_bounds {
|
||||
static_cast<LONG>(std::lround(item_min.x * client_width / io.DisplaySize.x)),
|
||||
static_cast<LONG>(std::lround(item_min.y * client_height / io.DisplaySize.y)),
|
||||
static_cast<LONG>(std::lround(item_max.x * client_width / io.DisplaySize.x)),
|
||||
static_cast<LONG>(std::lround(item_max.y * client_height / io.DisplaySize.y)),
|
||||
};
|
||||
games::nost::touch_mode::publish_button_bounds(
|
||||
this->overlay->get_window(), button_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "overlay/window.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// persistent mode control rendered independently of the main overlay visibility
|
||||
class NostalgiaTouchPiano : public Window {
|
||||
public:
|
||||
explicit NostalgiaTouchPiano(SpiceOverlay *overlay);
|
||||
|
||||
void calculate_initial_window() override;
|
||||
void build_content() override;
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "overlay/window.h"
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "patcher/patch_manager.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
enum class PatchType {
|
||||
Unknown,
|
||||
Memory,
|
||||
Signature,
|
||||
Union,
|
||||
Integer,
|
||||
};
|
||||
|
||||
enum class PatchStatus {
|
||||
Error,
|
||||
Disabled,
|
||||
Enabled,
|
||||
};
|
||||
|
||||
enum class PatchUrlStatus {
|
||||
Valid,
|
||||
Invalid,
|
||||
Unapplied,
|
||||
ValidButNoData,
|
||||
Partial,
|
||||
};
|
||||
|
||||
struct MemoryPatch {
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
|
||||
size_t data_disabled_len = 0;
|
||||
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
|
||||
size_t data_enabled_len = 0;
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t *data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchData;
|
||||
struct SignaturePatch {
|
||||
std::string dll_name = "";
|
||||
std::string signature = "", replacement = "";
|
||||
uint64_t offset = 0;
|
||||
int64_t usage = 0;
|
||||
|
||||
MemoryPatch to_memory(PatchData *patch);
|
||||
};
|
||||
|
||||
struct UnionPatch {
|
||||
std::string name = "";
|
||||
std::string dll_name = "";
|
||||
std::shared_ptr<uint8_t[]> data = nullptr;
|
||||
size_t data_len = 0;
|
||||
uint64_t offset = 0;
|
||||
uint8_t* data_offset_ptr = nullptr;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct NumberPatch {
|
||||
std::string dll_name = "";
|
||||
uint64_t data_offset = 0;
|
||||
uint8_t* data_offset_ptr = nullptr;
|
||||
int32_t min;
|
||||
int32_t max;
|
||||
int32_t value;
|
||||
size_t size_in_bytes;
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchData {
|
||||
bool enabled;
|
||||
std::string game_code;
|
||||
int datecode_min = 0;
|
||||
int datecode_max = 0;
|
||||
std::string name, description, caution;
|
||||
std::string name_in_lower_case = "";
|
||||
PatchType type;
|
||||
bool preset;
|
||||
std::vector<MemoryPatch> patches_memory;
|
||||
std::vector<UnionPatch> patches_union;
|
||||
NumberPatch patch_number;
|
||||
PatchStatus last_status;
|
||||
std::string hash;
|
||||
bool unverified = false;
|
||||
std::string peIdentifier;
|
||||
std::string error_reason = "";
|
||||
|
||||
// for union patch only
|
||||
std::string selected_union_name = "";
|
||||
};
|
||||
|
||||
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||
|
||||
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info=false);
|
||||
|
||||
class PatchManager : public Window {
|
||||
public:
|
||||
|
||||
PatchManager(SpiceOverlay *overlay, bool apply_patches = false);
|
||||
explicit PatchManager(SpiceOverlay *overlay);
|
||||
~PatchManager() override;
|
||||
|
||||
void build_content() override;
|
||||
void reload_local_patches(bool apply_patches = false);
|
||||
bool import_remote_patches_to_disk();
|
||||
bool load_from_patches_json(bool apply_patches);
|
||||
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
||||
void hard_apply_patches();
|
||||
void load_embedded_patches(bool apply_patches);
|
||||
|
||||
private:
|
||||
|
||||
// configuration
|
||||
static std::filesystem::path config_path;
|
||||
static bool config_dirty;
|
||||
static bool setting_auto_apply;
|
||||
static std::vector<std::string> setting_auto_apply_list;
|
||||
static std::vector<std::string> setting_patches_enabled;
|
||||
static std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
static std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
static std::string patch_url;
|
||||
static std::string patch_name_filter;
|
||||
|
||||
static std::filesystem::path LOCAL_PATCHES_PATH;
|
||||
static std::string ACTIVE_JSON_FILE;
|
||||
|
||||
// patches
|
||||
static std::vector<PatchData> patches;
|
||||
static bool local_patches_initialized;
|
||||
|
||||
// cached sorted view of `patches` for the table, stored as indices so a
|
||||
// stale entry can never dangle if `patches` is rebuilt; rebuilt only when
|
||||
// the sort order changes or the patch list is reloaded (not every frame)
|
||||
static std::vector<size_t> patches_sorted;
|
||||
|
||||
void config_load();
|
||||
void config_save();
|
||||
|
||||
// rebuild the cached sorted view of `patches` from the active table's
|
||||
// sort specs; no-op unless the sort changed or the patch list changed
|
||||
void update_sorted_patches();
|
||||
|
||||
void append_patches(
|
||||
std::string &patches_json,
|
||||
bool apply_patches = false,
|
||||
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
||||
std::string pe_identifier_for_patch = "");
|
||||
|
||||
void show_patch_tooltip(const PatchData& patch);
|
||||
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
||||
void show_patch_tooltip(const patcher::PatchData& patch);
|
||||
};
|
||||
|
||||
PatchStatus is_patch_active(PatchData &patch);
|
||||
bool apply_patch(PatchData &patch, bool active);
|
||||
|
||||
int64_t parse_little_endian_int(uint8_t* bytes, size_t size);
|
||||
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size);
|
||||
|
||||
std::vector<uint8_t>* find_in_dll_map(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
std::vector<uint8_t>* find_in_dll_map_org(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
bool restore_bytes_from_dll_map_org(
|
||||
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
void create_dll_backup(
|
||||
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
||||
std::string fix_up_dll_name(const std::string& dll_name);
|
||||
uint8_t* get_dll_offset_for_patch_apply(
|
||||
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes);
|
||||
|
||||
uint64_t parse_json_data_offset(
|
||||
const std::string &patch_name, const rapidjson::Value &value);
|
||||
|
||||
void print_auto_apply_status(PatchData &patch);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "external/hash-library/sha256.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "external/rapidjson/prettywriter.h"
|
||||
#include "external/rapidjson/stringbuffer.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace patcher {
|
||||
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config) {
|
||||
return ((id_from_config.compare(0, 3, avs::game::MODEL) == 0) &&
|
||||
(id_from_config.compare(10, 10, avs::game::EXT) == 0));
|
||||
}
|
||||
|
||||
void config_load() {
|
||||
log_info("patchmanager", "loading config");
|
||||
|
||||
// read config file
|
||||
std::string config = fileutils::text_read(config_path);
|
||||
if (!config.empty()) {
|
||||
|
||||
// parse document
|
||||
Document doc;
|
||||
doc.Parse(config.c_str());
|
||||
|
||||
// check parse error
|
||||
auto error = doc.GetParseError();
|
||||
if (error) {
|
||||
log_warning("patchmanager", "config file parse error: {}", static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
// verify root is a dict
|
||||
if (doc.IsObject()) {
|
||||
|
||||
// read auto apply settings
|
||||
auto auto_apply = doc.FindMember("auto_apply");
|
||||
if (auto_apply != doc.MemberEnd() && auto_apply->value.IsArray()) {
|
||||
|
||||
// get game id
|
||||
const auto game_id = avs::game::get_identifier();
|
||||
|
||||
// iterate entries
|
||||
setting_auto_apply = false;
|
||||
setting_auto_apply_list.clear();
|
||||
for (auto &entry : auto_apply->value.GetArray()) {
|
||||
if (entry.IsString()) {
|
||||
|
||||
// check if this is our game identifier
|
||||
const std::string entry_id = entry.GetString();
|
||||
|
||||
if (!setting_auto_apply) {
|
||||
if (game_id == entry_id) {
|
||||
// exact match
|
||||
setting_auto_apply = true;
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"matched auto apply entry by full game identifier: {}",
|
||||
entry_id);
|
||||
|
||||
} else if (is_game_id_wildcard_matched(entry_id)) {
|
||||
// match on model and ext, ignoring dest/spec/rev
|
||||
// sample: LDJ:J:E:A:2025011400
|
||||
setting_auto_apply = true;
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"matched auto apply entry by partial game identifier: {}:?:?:?:{}",
|
||||
avs::game::MODEL, avs::game::EXT);
|
||||
}
|
||||
}
|
||||
|
||||
// move to list
|
||||
setting_auto_apply_list.emplace_back(entry_id);
|
||||
}
|
||||
}
|
||||
if (!setting_auto_apply) {
|
||||
log_misc(
|
||||
"patchmanager",
|
||||
"no auto apply entry matched, patches will not load automatically");
|
||||
}
|
||||
}
|
||||
|
||||
// read enabled patches
|
||||
auto patches_enabled = doc.FindMember("patches_enabled");
|
||||
if (patches_enabled != doc.MemberEnd() && patches_enabled->value.IsArray()) {
|
||||
setting_patches_enabled.clear();
|
||||
for (const auto &patch : patches_enabled->value.GetArray()) {
|
||||
if (patch.IsString()) {
|
||||
setting_patches_enabled.emplace_back(std::string(patch.GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// read enabled union patches
|
||||
auto patches_union_enabled = doc.FindMember("union_patches_enabled");
|
||||
if (patches_union_enabled != doc.MemberEnd() && patches_union_enabled->value.IsObject()) {
|
||||
setting_union_patches_enabled.clear();
|
||||
for (auto it = patches_union_enabled->value.MemberBegin(); it != patches_union_enabled->value.MemberEnd(); ++it) {
|
||||
if (it->name.IsString() && it->value.IsString()) {
|
||||
setting_union_patches_enabled[it->name.GetString()] = it->value.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
// read enabled integer patches
|
||||
auto patches_int_enabled = doc.FindMember("integer_patches_enabled");
|
||||
if (patches_int_enabled != doc.MemberEnd() && patches_int_enabled->value.IsObject()) {
|
||||
setting_int_patches_enabled.clear();
|
||||
for (auto it = patches_int_enabled->value.MemberBegin(); it != patches_int_enabled->value.MemberEnd(); ++it) {
|
||||
if (it->name.IsString() && it->value.IsNumber()) {
|
||||
setting_int_patches_enabled[it->name.GetString()] = it->value.GetInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read remote patch URLs
|
||||
auto remote_url_history = doc.FindMember("remote_url_history");
|
||||
if (remote_url_history != doc.MemberEnd() && remote_url_history->value.IsArray()) {
|
||||
url_recents.clear();
|
||||
for (const auto &url : remote_url_history->value.GetArray()) {
|
||||
if (url.IsString()) {
|
||||
url_recents.emplace_back(std::string(url.GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string patch_hash(PatchData &patch) {
|
||||
SHA256 hash;
|
||||
hash.add(patch.game_code.c_str(), patch.game_code.length());
|
||||
if (patch.datecode_min != 0 || patch.datecode_max != 0) {
|
||||
hash.add(&patch.datecode_min, sizeof(patch.datecode_min));
|
||||
hash.add(&patch.datecode_max, sizeof(patch.datecode_max));
|
||||
}
|
||||
if (!patch.peIdentifier.empty()) {
|
||||
hash.add(patch.peIdentifier.c_str(), patch.peIdentifier.length());
|
||||
}
|
||||
hash.add(patch.name.c_str(), patch.name.length());
|
||||
hash.add(patch.description.c_str(), patch.description.length());
|
||||
return hash.getHash();
|
||||
}
|
||||
|
||||
void config_save() {
|
||||
|
||||
// create document
|
||||
Document doc;
|
||||
doc.Parse(
|
||||
"{"
|
||||
" \"auto_apply\": [],"
|
||||
" \"patches_enabled\": [],"
|
||||
" \"union_patches_enabled\": {},"
|
||||
" \"integer_patches_enabled\": {},"
|
||||
" \"remote_url_history\": []"
|
||||
"}"
|
||||
);
|
||||
|
||||
// check parse error
|
||||
auto error = doc.GetParseError();
|
||||
if (error) {
|
||||
log_warning("patchmanager", "template parse error: {}", static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
// auto apply setting
|
||||
auto &auto_apply_list = doc["auto_apply"];
|
||||
auto game_id = avs::game::get_identifier();
|
||||
bool game_id_added = false;
|
||||
for (auto &entry : setting_auto_apply_list) {
|
||||
if (entry == game_id || is_game_id_wildcard_matched(entry)) {
|
||||
if (!setting_auto_apply) {
|
||||
continue;
|
||||
}
|
||||
game_id_added = true;
|
||||
}
|
||||
auto_apply_list.PushBack(StringRef(entry.c_str()), doc.GetAllocator());
|
||||
}
|
||||
if (setting_auto_apply && !game_id_added) {
|
||||
auto_apply_list.PushBack(StringRef(game_id.c_str()), doc.GetAllocator());
|
||||
}
|
||||
|
||||
// get enabled patches
|
||||
auto &doc_patches_enabled = doc["patches_enabled"];
|
||||
auto &doc_union_patches_enable = doc["union_patches_enabled"];
|
||||
auto &doc_int_patches_enable = doc["integer_patches_enabled"];
|
||||
for (auto &patch : patches) {
|
||||
auto hash = patch_hash(patch);
|
||||
|
||||
if (patch.type == PatchType::Union) {
|
||||
// enable hash if known as enabled, overridden and missing from list
|
||||
if (patch.enabled) {
|
||||
setting_union_patches_enabled[hash] = patch.selected_union_name;
|
||||
} else {
|
||||
setting_union_patches_enabled.erase(hash);
|
||||
}
|
||||
} else if (patch.type == PatchType::Integer) {
|
||||
if (patch.enabled) {
|
||||
setting_int_patches_enabled[hash] = patch.patch_number.value;
|
||||
} else {
|
||||
setting_int_patches_enabled.erase(hash);
|
||||
}
|
||||
} else {
|
||||
// hash patch and find entry
|
||||
auto entry = std::find(setting_patches_enabled.begin(), setting_patches_enabled.end(), hash);
|
||||
|
||||
// enable hash if known as enabled, overridden and missing from list
|
||||
if ((patch.last_status == PatchStatus::Enabled && patch.enabled)
|
||||
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && patch.enabled)) {
|
||||
if (entry == setting_patches_enabled.end()) {
|
||||
setting_patches_enabled.emplace_back(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// disable hash if patch known as disabled
|
||||
if (patch.last_status == PatchStatus::Disabled
|
||||
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && !patch.enabled)) {
|
||||
if (entry != setting_patches_enabled.end()) {
|
||||
setting_patches_enabled.erase(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add hashes to document
|
||||
for (auto &hash : setting_patches_enabled) {
|
||||
Value hash_value(hash.c_str(), doc.GetAllocator());
|
||||
doc_patches_enabled.PushBack(hash_value, doc.GetAllocator());
|
||||
}
|
||||
|
||||
for (auto& it : setting_union_patches_enabled) {
|
||||
const std::string& key = it.first;
|
||||
const std::string& val = it.second;
|
||||
doc_union_patches_enable.AddMember(StringRef(key.c_str()), StringRef(val.c_str()), doc.GetAllocator());
|
||||
}
|
||||
|
||||
for (auto& it : setting_int_patches_enabled) {
|
||||
const std::string& key = it.first;
|
||||
const int32_t& val = it.second;
|
||||
doc_int_patches_enable.AddMember(StringRef(key.c_str()), val, doc.GetAllocator());
|
||||
}
|
||||
|
||||
// remote URLs
|
||||
auto &doc_url_history = doc["remote_url_history"];
|
||||
for (auto& url : url_recents) {
|
||||
Value url_value(url.c_str(), doc.GetAllocator());
|
||||
doc_url_history.PushBack(url_value, doc.GetAllocator());
|
||||
}
|
||||
|
||||
// build JSON
|
||||
StringBuffer buffer;
|
||||
PrettyWriter<StringBuffer> writer(buffer);
|
||||
doc.Accept(writer);
|
||||
|
||||
// save to file
|
||||
if (fileutils::write_config_file("patchmanager", config_path, buffer.GetString())) {
|
||||
config_dirty = false;
|
||||
} else {
|
||||
log_warning("patchmanager", "unable to save config file");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "patch_manager.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/nt_loader.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
// internal engine state (patcher module only)
|
||||
extern std::vector<std::string> setting_auto_apply_list;
|
||||
extern std::vector<std::string> setting_patches_enabled;
|
||||
extern std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
extern std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
extern std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
extern std::filesystem::path LOCAL_PATCHES_PATH;
|
||||
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
||||
extern bool ldr_registered;
|
||||
extern void *ldr_notify_cookie;
|
||||
extern std::vector<std::string> ldr_target_libraries;
|
||||
|
||||
// internal helpers
|
||||
void append_patches(
|
||||
std::string& patches_json,
|
||||
bool apply_patches = false,
|
||||
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
||||
std::string pe_identifier_for_patch = "");
|
||||
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
||||
std::string getFromUrl(const std::string& dll_name, const std::string& url);
|
||||
bool load_from_patches_json(bool apply_patches);
|
||||
void load_embedded_patches(bool apply_patches);
|
||||
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
||||
bool is_patch_group_definition(const rapidjson::Value& patch);
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
|
||||
const rapidjson::Document& doc);
|
||||
std::string resolve_patch_group_id(
|
||||
const rapidjson::Value& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const char *patch_name);
|
||||
void register_patch_group(
|
||||
PatchData& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions);
|
||||
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
|
||||
|
||||
std::string patch_hash(PatchData& patch);
|
||||
void clear_dll_maps();
|
||||
|
||||
int64_t parse_little_endian_int(uint8_t *bytes, size_t size);
|
||||
void int_to_little_endian_bytes(int64_t value, uint8_t *bytes, size_t size);
|
||||
|
||||
std::vector<uint8_t> *find_in_dll_map(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
std::vector<uint8_t> *find_in_dll_map_org(
|
||||
const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
bool restore_bytes_from_dll_map_org(
|
||||
uint8_t *destination, const std::string& dll_name, size_t offset, size_t size);
|
||||
|
||||
void create_dll_backup(
|
||||
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
||||
std::string fix_up_dll_name(const std::string& dll_name);
|
||||
uint8_t *get_dll_offset_for_patch_apply(
|
||||
const std::string& dll_name, uint64_t data_offset, size_t size_in_bytes);
|
||||
|
||||
uint64_t parse_json_data_offset(
|
||||
const std::string& patch_name, const rapidjson::Value& value);
|
||||
|
||||
void print_auto_apply_status(PatchData& patch);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||
|
||||
std::filesystem::path config_path;
|
||||
bool config_dirty = false;
|
||||
bool setting_auto_apply = false;
|
||||
|
||||
std::vector<std::string> setting_auto_apply_list;
|
||||
std::vector<std::string> setting_patches_enabled;
|
||||
|
||||
std::map<std::string, std::string> setting_union_patches_enabled;
|
||||
std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||
|
||||
std::string patch_url;
|
||||
|
||||
std::filesystem::path LOCAL_PATCHES_PATH("patches");
|
||||
std::string ACTIVE_JSON_FILE;
|
||||
|
||||
std::vector<PatchData> patches;
|
||||
bool local_patches_initialized = false;
|
||||
std::vector<size_t> patches_sorted;
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
|
||||
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
||||
{"arkmdxp3.dll", {"gamemdx.dll"}},
|
||||
{"arkmdxp4.dll", {"gamemdx.dll"}},
|
||||
{"arkmdxbio2.dll", {"gamemdx.dll"}},
|
||||
{"arkndd.dll", {"gamendd.dll"}},
|
||||
{"arkkep.dll", {"game.dll"}},
|
||||
{"arkjc9.dll", {"gamejc9.dll"}},
|
||||
{"arkkdm.dll", {"gamekdm.dll"}},
|
||||
{"arkmmd.dll", {"gamemmd.dll"}},
|
||||
{"arkklp.dll", {"lpac.dll"}},
|
||||
{"arknck.dll", {"weac.dll"}},
|
||||
{"gdxg.dll", {"game.dll", "libshare-pj.dll", "boot.dll"}}
|
||||
};
|
||||
|
||||
std::string url_fetch_errors;
|
||||
size_t url_recent_idx = -1;
|
||||
std::vector<std::string> url_recents;
|
||||
|
||||
bool ldr_registered = false;
|
||||
void *ldr_notify_cookie = nullptr;
|
||||
std::vector<std::string> ldr_target_libraries;
|
||||
|
||||
void init() {
|
||||
if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) {
|
||||
config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value();
|
||||
log_info("patchmanager", "using custom config file path: {}", config_path);
|
||||
} else {
|
||||
config_path =
|
||||
fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json");
|
||||
}
|
||||
|
||||
// register for DLL load notifications so patches can be (re)applied as
|
||||
// the game's target libraries come into memory. registration happens
|
||||
// once and is left in place for the lifetime of the process.
|
||||
if (!ldr_registered) {
|
||||
ldr_target_libraries = getExtraDlls(avs::game::DLL_NAME);
|
||||
ldr_target_libraries.push_back(avs::game::DLL_NAME);
|
||||
|
||||
const auto register_fn = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
|
||||
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
|
||||
|
||||
if (register_fn && NT_SUCCESS(register_fn(
|
||||
0, &loader_notification, nullptr, &ldr_notify_cookie))) {
|
||||
log_info("patchmanager", "registered for DLL load notifications");
|
||||
} else {
|
||||
log_warning("patchmanager", "failed to register for DLL load notifications");
|
||||
}
|
||||
|
||||
ldr_registered = true;
|
||||
}
|
||||
|
||||
if (!local_patches_initialized) {
|
||||
patch_url.clear();
|
||||
if (fileutils::file_exists(config_path)) {
|
||||
config_load();
|
||||
}
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_patches_on_start() {
|
||||
if (!local_patches_initialized) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
|
||||
VOID CALLBACK loader_notification(
|
||||
ULONG reason,
|
||||
PCLDR_DLL_NOTIFICATION_DATA data,
|
||||
PVOID) {
|
||||
|
||||
if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
|
||||
const auto dll = strtolower(std::filesystem::path({
|
||||
data->Loaded.FullDllName->Buffer,
|
||||
data->Loaded.FullDllName->Length / sizeof(wchar_t)
|
||||
}).filename().string());
|
||||
|
||||
if (std::ranges::find(ldr_target_libraries, dll) != ldr_target_libraries.end()) {
|
||||
reload_local_patches(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user