From 3b29227dbc424bb05e6957d0a24fef056424473f Mon Sep 17 00:00:00 2001
From: James Liu <17558260+JamesLewisLiu@users.noreply.github.com>
Date: Wed, 12 Aug 2026 15:08:05 +0800
Subject: [PATCH 01/19] gitadora: fix XG2 gibberish text showing (#863)
> [!NOTE]
> Before submitting code changes...
> * Please do note that this is a GPL v3.0 open source project.
> * Please read the
[CONTRIBUTING](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md)
guide.
> * Maintainers reserve the right to reject or modify your submission
without reason.
> * No new compiler warnings must be introduced. Check the CI build
results.
>
> Feel free to remove this section after you have read it.
## Link to GitHub Issue or related Pull Request, if one exists
## Description of change
Fix gibberish in dynamic Japanese text in GuitarFreaks/DrumMania XG2
(K32/K33) when Windows uses a non-Japanese system code page.
XG2 converts UTF-8 property strings through WideCharToMultiByte(CP_ACP)
before rendering. On systems where the active code page is not
Shift-JIS, these strings are converted using the system code page and
become corrupted or turn into question marks.
Spice2x already hooks WideCharToMultiByte and redirects these
conversions to CP932 for newer 64-bit Gitadora models. This change makes
the existing hook available to 32-bit builds and enables it specifically
for XG2 models K32 and K33.
The existing 64-bit Gitadora Arena and T44 conditions remain unchanged.
## Testing
Before
After
- Tested GuitarFreaks XG2 on a non-Japanese (English US) Windows.
- Verified that Community Log preset comments render correctly in
Japanese.
- Verified that Cooperation Challenge descriptions, rewards, and
progress text render correctly.
- Confirmed that the previous mojibake and question marks no longer
appear.
- GitHub Actions build completed successfully for both architectures.
---
src/spice2x/hooks/lang.cpp | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/src/spice2x/hooks/lang.cpp b/src/spice2x/hooks/lang.cpp
index 553b966..0567dc2 100644
--- a/src/spice2x/hooks/lang.cpp
+++ b/src/spice2x/hooks/lang.cpp
@@ -26,13 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
static decltype(GetACP) *GetACP_orig = nullptr;
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
+static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
-static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
#endif
@@ -209,6 +209,8 @@ static BOOL WINAPI IsDBCSLeadByteEx_hook(
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
}
+#endif
+
static
int
WINAPI
@@ -244,6 +246,8 @@ WideCharToMultiByte_hook(
lpUsedDefaultChar);
}
+#ifdef SPICE64
+
int
WINAPI
GetLocaleInfoA_hook(
@@ -343,15 +347,6 @@ void hooks::lang::early_init() {
&IsDBCSLeadByte_orig);
}
- if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) {
- log_info("hooks::lang", "hooking WideCharToMultiByte");
- detour::trampoline_try(
- "kernel32.dll",
- "WideCharToMultiByte",
- WideCharToMultiByte_hook,
- &WideCharToMultiByte_orig);
- }
-
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
detour::trampoline_try(
"kernel32.dll",
@@ -362,6 +357,23 @@ void hooks::lang::early_init() {
#endif
+#ifdef SPICE64
+ const auto hook_wide_char_to_multi_byte =
+ games::gitadora::is_arena_model() || avs::game::is_model("T44");
+#else
+ // XG2 converts UTF-8 property strings through CP_ACP before rendering.
+ const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
+#endif
+
+ if (hook_wide_char_to_multi_byte) {
+ log_info("hooks::lang", "hooking WideCharToMultiByte");
+ detour::trampoline_try(
+ "kernel32.dll",
+ "WideCharToMultiByte",
+ WideCharToMultiByte_hook,
+ &WideCharToMultiByte_orig);
+ }
+
}
void hooks::lang::init() {
From 82e0c053d049aad74ec5d5a49bfca253eaa5a047 Mon Sep 17 00:00:00 2001
From: bicarus <202771338+bicarus-dev@users.noreply.github.com>
Date: Wed, 12 Aug 2026 20:04:59 -0700
Subject: [PATCH 02/19] overlay: create a dedicated thread for polling hotkeys
(#864)
## Link to GitHub Issue or related Pull Request, if one exists
n/a
## Description of change
Create a new thread that polls for the following hotkeys:
* super exit (both alt+f4 and bound key)
* coin insert
* screenshot
The goal is to make the capture of these more reliable, because before
this PR it wasn't.
## Testing
---
src/spice2x/CMakeLists.txt | 1 +
.../backends/d3d11/d3d11_swapchain.cpp | 19 +--
.../graphics/backends/d3d9/d3d9_backend.cpp | 2 +-
.../backends/d3d9/d3d9_screenshot.cpp | 19 ---
.../graphics/backends/d3d9/d3d9_screenshot.h | 2 -
src/spice2x/hooks/graphics/graphics.cpp | 8 +
src/spice2x/hooks/graphics/graphics.h | 1 +
src/spice2x/launcher/launcher.cpp | 25 +--
src/spice2x/launcher/superexit.cpp | 113 ++-----------
src/spice2x/launcher/superexit.h | 3 +-
src/spice2x/misc/eamuse.cpp | 79 +++------
src/spice2x/misc/eamuse.h | 4 +-
src/spice2x/misc/hotkeys.cpp | 154 ++++++++++++++++++
src/spice2x/misc/hotkeys.h | 13 ++
src/spice2x/overlay/overlay.cpp | 8 +
src/spice2x/overlay/overlay.h | 2 +
src/spice2x/rawinput/rawinput.cpp | 25 +++
src/spice2x/rawinput/rawinput.h | 1 +
18 files changed, 272 insertions(+), 207 deletions(-)
create mode 100644 src/spice2x/misc/hotkeys.cpp
create mode 100644 src/spice2x/misc/hotkeys.h
diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt
index 9fa0c0e..addc95e 100644
--- a/src/spice2x/CMakeLists.txt
+++ b/src/spice2x/CMakeLists.txt
@@ -578,6 +578,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
+ misc/hotkeys.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
index c7a8b00..6b00a48 100644
--- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
+++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
@@ -23,10 +23,7 @@
#include "external/imgui/backends/imgui_impl_dx11.h"
#include "overlay/imgui/impl_spice.h"
-#include "games/io.h"
#include "hooks/graphics/graphics.h"
-#include "launcher/launcher.h"
-#include "misc/eamuse.h"
#include "util/utils.h"
// --------------------------------------------------------------------------
@@ -149,26 +146,12 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
device->Release();
}
-// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
-void poll_screenshot_hotkey() {
- static bool s_down = false;
- auto buttons = games::get_buttons_overlay(eamuse_get_game());
- const bool pressed = buttons
- && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
- && GameAPI::Buttons::getState(RI_MGR,
- buttons->at(games::OverlayButtons::Screenshot));
- if (pressed && !s_down) {
- graphics_screenshot_trigger();
- }
- s_down = pressed;
-}
-
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
return;
}
- poll_screenshot_hotkey();
+ graphics_poll_screenshot_hotkey();
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp
index 17755d1..1bf5a60 100644
--- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp
+++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp
@@ -1522,7 +1522,7 @@ void graphics_d3d9_on_present(
wintouchemu::update();
}
- graphics_d3d9_poll_screenshot_hotkey();
+ graphics_poll_screenshot_hotkey();
graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN);
}
diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
index 4b53643..aa94645 100644
--- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
+++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
@@ -13,13 +13,9 @@
#endif
#include "avs/game.h"
-#include "games/io.h"
#include "hooks/graphics/graphics.h"
-#include "launcher/launcher.h"
#include "misc/clipboard.h"
-#include "misc/eamuse.h"
#include "overlay/notifications.h"
-#include "overlay/overlay.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/logging.h"
@@ -240,21 +236,6 @@ static void save_screenshot(
}
}
-void graphics_d3d9_poll_screenshot_hotkey() {
- static bool trigger_last = false;
- auto buttons = games::get_buttons_overlay(eamuse_get_game());
- if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
- GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot)))
- {
- if (!trigger_last) {
- graphics_screenshot_trigger();
- }
- trigger_last = true;
- } else {
- trigger_last = false;
- }
-}
-
static std::optional acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *sub_swap_chain, int screen) {
diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h
index efa9096..50ac633 100644
--- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h
+++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h
@@ -2,8 +2,6 @@
#include
-void graphics_d3d9_poll_screenshot_hotkey();
-
void graphics_d3d9_process_screenshot_and_capture(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain);
diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp
index 7e91ad4..0416c66 100644
--- a/src/spice2x/hooks/graphics/graphics.cpp
+++ b/src/spice2x/hooks/graphics/graphics.cpp
@@ -17,11 +17,13 @@
#include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
+#include "games/io.h"
#include "games/sdvx/sdvx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "launcher/shutdown.h"
+#include "misc/hotkeys.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_gestures.h"
@@ -1374,6 +1376,12 @@ void graphics_screens_get(std::vector &screens) {
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
}
+void graphics_poll_screenshot_hotkey() {
+ if (hotkeys::consume_screenshot()) {
+ graphics_screenshot_trigger();
+ }
+}
+
void graphics_screenshot_trigger() {
GRAPHICS_SCREENSHOT_TRIGGER = true;
}
diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h
index 22663bd..75376c0 100644
--- a/src/spice2x/hooks/graphics/graphics.h
+++ b/src/spice2x/hooks/graphics/graphics.h
@@ -134,6 +134,7 @@ void graphics_hook_subscreen_window(HWND hWnd);
void graphics_screens_register(int screen);
void graphics_screens_unregister(int screen);
void graphics_screens_get(std::vector &screens);
+void graphics_poll_screenshot_hotkey();
void graphics_screenshot_trigger();
bool graphics_screenshot_consume();
void graphics_capture_trigger(int screen);
diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp
index c768ee7..00fdf57 100644
--- a/src/spice2x/launcher/launcher.cpp
+++ b/src/spice2x/launcher/launcher.cpp
@@ -87,13 +87,13 @@
#include "launcher/launcher.h"
#include "launcher/logger.h"
#include "launcher/signal.h"
-#include "launcher/superexit.h"
#include "launcher/richpresence.h"
#include "launcher/shutdown.h"
#include "launcher/options.h"
#include "misc/bt5api.h"
#include "misc/device.h"
#include "misc/eamuse.h"
+#include "misc/hotkeys.h"
#include "misc/extdev.h"
#include "misc/ami2000.h"
#include "misc/sciunit.h"
@@ -1777,9 +1777,8 @@ int main_implementation(int argc, char *argv[]) {
nvapi::initialize();
// add application profile to nvcp
nvapi::set_profile_settings();
- // enable super exit
- superexit::enable();
-
+ // keep ALT+F4 available during lengthy non-standalone boot
+ hotkeys::start();
// enable subscreen touch emulation
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
games::iidx::ENABLE_POKE = true;
@@ -2452,6 +2451,7 @@ int main_implementation(int argc, char *argv[]) {
// initialize raw input
RI_MGR = std::make_unique();
+ hotkeys::enable_raw_input();
for (const auto &device : sextet_devices) {
RI_MGR->sextet_register(device);
}
@@ -2474,6 +2474,9 @@ int main_implementation(int argc, char *argv[]) {
log_misc("rawinput", "Analog mappings:");
dump_analog_bindings();
+ // mappings are ready; begin screenshot and coin polling during late startup
+ hotkeys::enable_input();
+
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
GRAPHICS_SHOW_CURSOR = true;
@@ -2703,9 +2706,6 @@ int main_implementation(int argc, char *argv[]) {
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
}
- // start coin input thread
- eamuse_coin_start_thread();
-
// pin macro
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
eamuse_pin_macro_start_thread();
@@ -2755,6 +2755,9 @@ int main_implementation(int argc, char *argv[]) {
log_info("launcher", "calling game entry");
avs::game::entry_main();
+ // stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
+ hotkeys::disable_input();
+
// clear presence
richpresence::shutdown();
@@ -2792,9 +2795,6 @@ int main_implementation(int argc, char *argv[]) {
// free api controller
API_CONTROLLER.reset();
- // stop coin input thread
- eamuse_coin_stop_thread();
-
eamuse_pin_macro_stop_thread();
// BT5API
@@ -2805,6 +2805,7 @@ int main_implementation(int argc, char *argv[]) {
sdk::fini_sdk_modules();
// stop raw input
+ hotkeys::disable_raw_input();
RI_MGR.reset();
// debug hook
@@ -2837,8 +2838,8 @@ int main_implementation(int argc, char *argv[]) {
// dispose crypt
crypt::dispose();
- // disable super exit
- superexit::disable();
+ // end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
+ hotkeys::stop();
// disable poke
games::iidx::poke::disable();
diff --git a/src/spice2x/launcher/superexit.cpp b/src/spice2x/launcher/superexit.cpp
index 5219905..9db98a1 100644
--- a/src/spice2x/launcher/superexit.cpp
+++ b/src/spice2x/launcher/superexit.cpp
@@ -1,21 +1,14 @@
#include "superexit.h"
-#include
-
#include "windows.h"
+
+#include "cfg/configurator.h"
#include "launcher/shutdown.h"
-#include "rawinput/rawinput.h"
-#include "util/logging.h"
#include "touch/touch.h"
-#include "misc/eamuse.h"
-#include "games/io.h"
-#include "overlay/overlay.h"
+#include "util/logging.h"
namespace superexit {
- static std::thread *THREAD = nullptr;
- static bool THREAD_RUNNING = false;
-
bool has_focus() {
HWND fg_wnd = GetForegroundWindow();
if (fg_wnd == NULL) {
@@ -29,92 +22,22 @@ namespace superexit {
return fg_pid == GetCurrentProcessId();
}
- void enable() {
-
- // check if already running
- if (THREAD)
- return;
-
- // create new thread
- THREAD_RUNNING = true;
- THREAD = new std::thread([] {
-
- // log
- log_info("superexit", "enabled");
-
- // set variable to false to stop
- while (THREAD_RUNNING) {
-
- // check rawinput for ALT+F4
- bool rawinput_exit = false;
- if (RI_MGR != nullptr) {
- auto devices = RI_MGR->devices_get();
- for (auto &device : devices) {
- switch (device.type) {
- case rawinput::KEYBOARD: {
- auto &key_states = device.keyboardInfo->key_states;
- for (int page_index = 0; page_index < 1024; page_index += 256) {
- if (key_states[page_index + VK_MENU]
- && key_states[page_index + VK_F4]) {
- rawinput_exit = true;
- }
- }
- break;
- }
- default:
- break;
- }
- }
- }
-
- bool async_key_exit =
- (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
- (GetAsyncKeyState(VK_F4) & 0x8000) != 0;
-
- bool overlay_exit = false;
- auto buttons = games::get_buttons_overlay(eamuse_get_game());
- if (buttons &&
- (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
- GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
- overlay_exit = true;
- }
-
- // check for exit
- if (rawinput_exit || async_key_exit) {
- if (has_focus()) {
- log_info("superexit", "detected ALT+F4, exiting...");
- launcher::shutdown();
- }
- }
- if (overlay_exit) {
- if (has_focus()) {
- log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
- launcher::shutdown();
- }
- }
-
- // slow down
- Sleep(100);
- }
-
- return nullptr;
- });
- }
-
- void disable() {
- if (!THREAD) {
+ void handle_hotkeys(bool alt_f4, bool mapped_exit) {
+ if (!alt_f4 && !mapped_exit) {
return;
}
-
- // stop old thread
- THREAD_RUNNING = false;
- THREAD->join();
-
- // delete thread
- delete THREAD;
- THREAD = nullptr;
-
- // log
- log_info("superexit", "disabled");
+ if (cfg::CONFIGURATOR_STANDALONE) {
+ return;
+ }
+ if (!has_focus()) {
+ return;
+ }
+ if (alt_f4) {
+ log_info("superexit", "detected ALT+F4, exiting...");
+ launcher::shutdown();
+ return;
+ }
+ log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
+ launcher::shutdown();
}
}
diff --git a/src/spice2x/launcher/superexit.h b/src/spice2x/launcher/superexit.h
index 193727a..575cd01 100644
--- a/src/spice2x/launcher/superexit.h
+++ b/src/spice2x/launcher/superexit.h
@@ -2,6 +2,5 @@
namespace superexit {
bool has_focus();
- void enable();
- void disable();
+ void handle_hotkeys(bool alt_f4, bool mapped_exit);
}
diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp
index a89d2ad..7e054f5 100644
--- a/src/spice2x/misc/eamuse.cpp
+++ b/src/spice2x/misc/eamuse.cpp
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
static double CARD_INSERT_TIMEOUT = 2.0;
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
-static int COIN_STOCK = 0;
-static bool COIN_BLOCK = false;
-static std::thread *COIN_INPUT_THREAD;
-static bool COIN_INPUT_THREAD_ACTIVE = false;
+static std::atomic_int COIN_STOCK {0};
+static std::atomic_bool COIN_BLOCK {false};
static uint16_t KEYPAD_STATE[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
@@ -292,78 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
}
bool eamuse_coin_get_block() {
- return COIN_BLOCK;
+ return COIN_BLOCK.load(std::memory_order_relaxed);
}
void eamuse_coin_set_block(bool block) {
- COIN_BLOCK = block;
+ COIN_BLOCK.store(block, std::memory_order_relaxed);
}
int eamuse_coin_get_stock() {
- return COIN_STOCK;
+ return COIN_STOCK.load(std::memory_order_relaxed);
}
void eamuse_coin_set_stock(int amount) {
- COIN_STOCK = amount;
+ COIN_STOCK.store(amount, std::memory_order_relaxed);
}
bool eamuse_coin_consume(int amount) {
- if (COIN_STOCK < amount) {
- return false;
- } else {
- COIN_STOCK -= amount;
- return true;
+ auto stock = COIN_STOCK.load(std::memory_order_relaxed);
+ while (stock >= amount) {
+ if (COIN_STOCK.compare_exchange_weak(
+ stock,
+ stock - amount,
+ std::memory_order_relaxed)) {
+ return true;
+ }
}
+ return false;
}
int eamuse_coin_consume_stock() {
- int stock = COIN_STOCK;
- COIN_STOCK = 0;
- return stock;
+ return COIN_STOCK.exchange(0, std::memory_order_relaxed);
}
int eamuse_coin_add() {
- return ++COIN_STOCK;
+ return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
}
-void eamuse_coin_start_thread() {
-
- // set active
- COIN_INPUT_THREAD_ACTIVE = true;
-
- // create thread
- COIN_INPUT_THREAD = new std::thread([]() {
- auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
- static bool COIN_INPUT_KEY_STATE = false;
- while (COIN_INPUT_THREAD_ACTIVE) {
-
- // check input key
- if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(
- games::OverlayButtons::InsertCoin))) {
- if (!COIN_INPUT_KEY_STATE) {
- if (COIN_BLOCK)
- log_info("eamuse", "coin inserted while blocked");
- else {
- log_info("eamuse", "coin insert");
- COIN_STOCK++;
- }
- }
- COIN_INPUT_KEY_STATE = true;
- } else {
- COIN_INPUT_KEY_STATE = false;
- }
-
- // once every two frames
- std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30));
- }
- });
-}
-
-void eamuse_coin_stop_thread() {
- COIN_INPUT_THREAD_ACTIVE = false;
- COIN_INPUT_THREAD->join();
- delete COIN_INPUT_THREAD;
- COIN_INPUT_THREAD = nullptr;
+void eamuse_coin_insert() {
+ if (COIN_BLOCK.load(std::memory_order_relaxed)) {
+ log_info("eamuse", "coin inserted while blocked");
+ } else {
+ log_info("eamuse", "coin insert");
+ COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
+ }
}
void eamuse_pin_macro_start_thread() {
diff --git a/src/spice2x/misc/eamuse.h b/src/spice2x/misc/eamuse.h
index b2bd51d..d772bb5 100644
--- a/src/spice2x/misc/eamuse.h
+++ b/src/spice2x/misc/eamuse.h
@@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount);
int eamuse_coin_consume_stock();
int eamuse_coin_add();
-
-void eamuse_coin_start_thread();
-void eamuse_coin_stop_thread();
+void eamuse_coin_insert();
void eamuse_pin_macro_start_thread();
void eamuse_pin_macro_stop_thread();
diff --git a/src/spice2x/misc/hotkeys.cpp b/src/spice2x/misc/hotkeys.cpp
new file mode 100644
index 0000000..7a21852
--- /dev/null
+++ b/src/spice2x/misc/hotkeys.cpp
@@ -0,0 +1,154 @@
+#include "hotkeys.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "games/io.h"
+#include "launcher/superexit.h"
+#include "misc/eamuse.h"
+#include "overlay/overlay.h"
+#include "rawinput/rawinput.h"
+#include "util/logging.h"
+
+namespace hotkeys {
+
+ namespace {
+
+ // 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
+ // interval during early boot or when process timer adjustments are disabled
+ constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
+
+ std::atomic_bool SCREENSHOT_PENDING {false};
+ std::mutex INPUT_MUTEX;
+ bool INPUT_ENABLED = false;
+ bool RAW_INPUT_ENABLED = false;
+ std::jthread WORKER;
+
+ bool read_button(std::vector