diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 3b18554..babe7b0 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -417,6 +417,7 @@ set(SOURCE_FILES ${SOURCE_FILES} 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 @@ -610,6 +611,7 @@ 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 diff --git a/src/spice2x/acio/panb/panb.cpp b/src/spice2x/acio/panb/panb.cpp index 3222c44..bd25fc4 100644 --- a/src/spice2x/acio/panb/panb.cpp +++ b/src/spice2x/acio/panb/panb.cpp @@ -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; diff --git a/src/spice2x/games/nost/nost.cpp b/src/spice2x/games/nost/nost.cpp index 6175f13..4bb582d 100644 --- a/src/spice2x/games/nost/nost.cpp +++ b/src/spice2x/games/nost/nost.cpp @@ -1,5 +1,6 @@ #include "nost.h" #include "poke.h" +#include "touch_mode.h" #include "hooks/setupapihook.h" #include "touch/native/nativetouchhook.h" #include "avs/game.h" @@ -7,6 +8,7 @@ namespace games::nost { bool ENABLE_POKE = false; + bool ENABLE_TOUCH_MODE = false; NostGame::NostGame() : Game("Nostalgia") { } @@ -40,6 +42,9 @@ namespace games::nost { setupapihook_add(touch_settings); nativetouch::hook(avs::game::DLL_INSTANCE); + if (ENABLE_TOUCH_MODE) { + touch_mode::enable(); + } } void NostGame::post_attach() { @@ -49,6 +54,9 @@ namespace games::nost { } void NostGame::detach() { + if (ENABLE_TOUCH_MODE) { + touch_mode::disable(); + } if (ENABLE_POKE) { poke::disable(); } diff --git a/src/spice2x/games/nost/nost.h b/src/spice2x/games/nost/nost.h index c46ce3f..0acca32 100644 --- a/src/spice2x/games/nost/nost.h +++ b/src/spice2x/games/nost/nost.h @@ -5,6 +5,7 @@ namespace games::nost { extern bool ENABLE_POKE; + extern bool ENABLE_TOUCH_MODE; class NostGame : public games::Game { public: diff --git a/src/spice2x/games/nost/touch_mode.cpp b/src/spice2x/games/nost/touch_mode.cpp new file mode 100644 index 0000000..dd918a3 --- /dev/null +++ b/src/spice2x/games/nost/touch_mode.cpp @@ -0,0 +1,233 @@ +#include "touch_mode.h" + +#include +#include +#include + +#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 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 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 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 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 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 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 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( + (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; + } +} diff --git a/src/spice2x/games/nost/touch_mode.h b/src/spice2x/games/nost/touch_mode.h new file mode 100644 index 0000000..7ef19fb --- /dev/null +++ b/src/spice2x/games/nost/touch_mode.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#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); +} diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index e5d4976..160acb0 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1786,7 +1786,16 @@ int main_implementation(int argc, char *argv[]) { games::iidx::poke::enable(); } if (options[launcher::Options::NostalgiaPoke].is_active()) { - games::nost::ENABLE_POKE = TRUE; + games::nost::ENABLE_POKE = true; + } + if (options[launcher::Options::NostalgiaTouchMode].is_active()) { + if (overlay::ENABLED) { + games::nost::ENABLE_TOUCH_MODE = true; + } else { + log_warning( + "launcher", + "Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch"); + } } } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 4a393c5..cf56864 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -3149,6 +3149,21 @@ static const std::vector 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 diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 9d78db5..ef912a5 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -297,6 +297,7 @@ namespace launcher { DDRP4IOBufferMode, InputRequiresFocus, NostalgiaPoke, + NostalgiaTouchMode, ForceBackBufferCount, SDVXWindowedSubscreenSize, SDVXWindowedSubscreenPosition, diff --git a/src/spice2x/overlay/overlay.cpp b/src/spice2x/overlay/overlay.cpp index 27e2c35..f0af959 100644 --- a/src/spice2x/overlay/overlay.cpp +++ b/src/spice2x/overlay/overlay.cpp @@ -5,6 +5,7 @@ #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" @@ -51,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" @@ -404,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(this); + } + // owned separately from `windows` so it is not part of the overlay window layer window_main_menu = std::make_unique(this); @@ -556,13 +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 && !draw_rb_touch_debug) { + if (!this->active && !draw_notifications && !draw_fps_persistent && + !draw_nostalgia_touch_piano && !draw_rb_touch_debug) { return; } @@ -596,7 +609,13 @@ void overlay::SpiceOverlay::new_frame() { 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(); @@ -819,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(); diff --git a/src/spice2x/overlay/overlay.h b/src/spice2x/overlay/overlay.h index 1d5b3f8..a1265c7 100644 --- a/src/spice2x/overlay/overlay.h +++ b/src/spice2x/overlay/overlay.h @@ -83,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_fps; + std::unique_ptr 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 @@ -138,6 +139,9 @@ namespace overlay { inline IDirect3DDevice9 *get_device() { return this->device; } + inline HWND get_window() { + return this->hWnd; + } bool can_transform_touch_input() { return (this->subscreen_mouse_handler != nullptr); diff --git a/src/spice2x/overlay/windows/nostalgia_touch_piano.cpp b/src/spice2x/overlay/windows/nostalgia_touch_piano.cpp new file mode 100644 index 0000000..4375ceb --- /dev/null +++ b/src/spice2x/overlay/windows/nostalgia_touch_piano.cpp @@ -0,0 +1,149 @@ +#include "nostalgia_touch_piano.h" + +#include + +#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(std::lround(item_min.x * client_width / io.DisplaySize.x)), + static_cast(std::lround(item_min.y * client_height / io.DisplaySize.y)), + static_cast(std::lround(item_max.x * client_width / io.DisplaySize.x)), + static_cast(std::lround(item_max.y * client_height / io.DisplaySize.y)), + }; + games::nost::touch_mode::publish_button_bounds( + this->overlay->get_window(), button_bounds); + } + } +} diff --git a/src/spice2x/overlay/windows/nostalgia_touch_piano.h b/src/spice2x/overlay/windows/nostalgia_touch_piano.h new file mode 100644 index 0000000..34bd5f2 --- /dev/null +++ b/src/spice2x/overlay/windows/nostalgia_touch_piano.h @@ -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; + }; +} diff --git a/src/spice2x/touch/native/nativetouchhook.cpp b/src/spice2x/touch/native/nativetouchhook.cpp index 855c023..b86d190 100644 --- a/src/spice2x/touch/native/nativetouchhook.cpp +++ b/src/spice2x/touch/native/nativetouchhook.cpp @@ -2,6 +2,9 @@ // mingw otherwise doesn't load touch stuff #define _WIN32_WINNT 0x0601 +#include + +#include "nativetouchhook.h" #include "avs/game.h" #include "rawinput/touch.h" #include "inject.h" @@ -32,6 +35,7 @@ namespace nativetouch { static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr; static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; + static std::atomic touch_input_filter { nullptr }; static bool native_touch_hooked = false; static bool native_display_initialized = false; static DWORD native_display_orientation = DMDO_DEFAULT; @@ -184,6 +188,22 @@ namespace nativetouch { point->dwFlags = 0; } } + + const auto filter = touch_input_filter.load(std::memory_order_acquire); + if (point->dwFlags != 0 && filter != nullptr) { + const NativeTouchEvent event { + .id = point->dwID, + .x = point->x / 100, + .y = point->y / 100, + .down = (point->dwFlags & TOUCHEVENTF_DOWN) != 0, + .move = (point->dwFlags & TOUCHEVENTF_MOVE) != 0, + .up = (point->dwFlags & TOUCHEVENTF_UP) != 0, + .synthetic = synthetic, + }; + if (filter(event)) { + point->dwFlags = 0; + } + } } return result; @@ -193,6 +213,10 @@ namespace nativetouch { return native_touch_hooked; } + void set_input_filter(TouchInputFilter filter) { + touch_input_filter.store(filter, std::memory_order_release); + } + void refresh_contact_lifetime() { if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) { inject::refresh_contact_lifetime(); diff --git a/src/spice2x/touch/native/nativetouchhook.h b/src/spice2x/touch/native/nativetouchhook.h index afbc1f5..e66045c 100644 --- a/src/spice2x/touch/native/nativetouchhook.h +++ b/src/spice2x/touch/native/nativetouchhook.h @@ -1,8 +1,23 @@ +#pragma once + #include namespace nativetouch { + struct NativeTouchEvent { + DWORD id; + LONG x; + LONG y; + bool down; + bool move; + bool up; + bool synthetic; + }; + + using TouchInputFilter = bool (*)(const NativeTouchEvent &event); + void hook(HMODULE module); void refresh_contact_lifetime(); + void set_input_filter(TouchInputFilter filter); // true once hook() has installed the native touch stack for the current game; // such games consume touch through the GetTouchInputInfo hook rather than spicetouch