nost: touch piano mode (#830)

## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Touch piano mode. Allows piano keys to be played with the touch screen.
Since the touch screen is still needed for menu navigation (including
during songs) a toggle switch is added so that the user can flip between
navigation mode and piano mode.

The touch targets are pixel-perfect. Don't let the game's piano
animations fool you.

## Testing
This commit is contained in:
bicarus
2026-07-23 15:49:50 -07:00
committed by GitHub
parent 80deb4bbbd
commit 64600826b7
15 changed files with 540 additions and 2 deletions
+2
View File
@@ -417,6 +417,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/nost/nost.cpp games/nost/nost.cpp
games/nost/io.cpp games/nost/io.cpp
games/nost/poke.cpp games/nost/poke.cpp
games/nost/touch_mode.cpp
games/gitadora/gitadora.cpp games/gitadora/gitadora.cpp
games/gitadora/asio.cpp games/gitadora/asio.cpp
games/gitadora/io.cpp games/gitadora/io.cpp
@@ -610,6 +611,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp overlay/windows/keypad.cpp
overlay/windows/log.cpp overlay/windows/log.cpp
overlay/windows/midi.cpp overlay/windows/midi.cpp
overlay/windows/nostalgia_touch_piano.cpp
overlay/windows/obs.cpp overlay/windows/obs.cpp
overlay/windows/obs_websocket.cpp overlay/windows/obs_websocket.cpp
overlay/windows/patch_manager.cpp overlay/windows/patch_manager.cpp
+12
View File
@@ -3,6 +3,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/nost/io.h" #include "games/nost/io.h"
#include "games/nost/nost.h" #include "games/nost/nost.h"
#include "games/nost/touch_mode.h"
#include "util/logging.h" #include "util/logging.h"
#include "avs/game.h" #include "avs/game.h"
@@ -16,6 +17,7 @@ using namespace GameAPI;
// static stuff // static stuff
static uint8_t STATUS_BUFFER[277]; static uint8_t STATUS_BUFFER[277];
static bool STATUS_BUFFER_FREEZE = false; static bool STATUS_BUFFER_FREEZE = false;
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
/* /*
* Implementations * Implementations
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
games::nost::Analogs::Key27, games::nost::Analogs::Key28, games::nost::Analogs::Key27, games::nost::Analogs::Key28,
}; };
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
// iterate pairs of keys // iterate pairs of keys
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) { 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) { if (button0 > 0) {
state0 = button0; 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( const auto button1 = panb_get_button_velocity(
buttons.at(button_mapping[key_pair * 2 + 1]), 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) { if (button1 > 0) {
state1 = button1; 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 // build value
uint8_t value = 0; uint8_t value = 0;
+8
View File
@@ -1,5 +1,6 @@
#include "nost.h" #include "nost.h"
#include "poke.h" #include "poke.h"
#include "touch_mode.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "avs/game.h" #include "avs/game.h"
@@ -7,6 +8,7 @@
namespace games::nost { namespace games::nost {
bool ENABLE_POKE = false; bool ENABLE_POKE = false;
bool ENABLE_TOUCH_MODE = false;
NostGame::NostGame() : Game("Nostalgia") { NostGame::NostGame() : Game("Nostalgia") {
} }
@@ -40,6 +42,9 @@ namespace games::nost {
setupapihook_add(touch_settings); setupapihook_add(touch_settings);
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (ENABLE_TOUCH_MODE) {
touch_mode::enable();
}
} }
void NostGame::post_attach() { void NostGame::post_attach() {
@@ -49,6 +54,9 @@ namespace games::nost {
} }
void NostGame::detach() { void NostGame::detach() {
if (ENABLE_TOUCH_MODE) {
touch_mode::disable();
}
if (ENABLE_POKE) { if (ENABLE_POKE) {
poke::disable(); poke::disable();
} }
+1
View File
@@ -5,6 +5,7 @@
namespace games::nost { namespace games::nost {
extern bool ENABLE_POKE; extern bool ENABLE_POKE;
extern bool ENABLE_TOUCH_MODE;
class NostGame : public games::Game { class NostGame : public games::Game {
public: public:
+233
View File
@@ -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;
}
}
+27
View File
@@ -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);
}
+10 -1
View File
@@ -1786,7 +1786,16 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::poke::enable(); games::iidx::poke::enable();
} }
if (options[launcher::Options::NostalgiaPoke].is_active()) { 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");
}
} }
} }
+15
View File
@@ -3149,6 +3149,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Nostalgia", .game_name = "Nostalgia",
.category = "Game Options", .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 // ForceBackBufferCount
+1
View File
@@ -297,6 +297,7 @@ namespace launcher {
DDRP4IOBufferMode, DDRP4IOBufferMode,
InputRequiresFocus, InputRequiresFocus,
NostalgiaPoke, NostalgiaPoke,
NostalgiaTouchMode,
ForceBackBufferCount, ForceBackBufferCount,
SDVXWindowedSubscreenSize, SDVXWindowedSubscreenSize,
SDVXWindowedSubscreenPosition, SDVXWindowedSubscreenPosition,
+24 -1
View File
@@ -5,6 +5,7 @@
#include "games/io.h" #include "games/io.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/nost/nost.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "games/rb/touch_debug.h" #include "games/rb/touch_debug.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
@@ -51,6 +52,7 @@
#include "windows/sdvx_sub.h" #include "windows/sdvx_sub.h"
#include "windows/keypad.h" #include "windows/keypad.h"
#include "windows/log.h" #include "windows/log.h"
#include "windows/nostalgia_touch_piano.h"
#include "windows/obs.h" #include "windows/obs.h"
#include "windows/patch_manager.h" #include "windows/patch_manager.h"
#include "windows/exitprompt.cpp" #include "windows/exitprompt.cpp"
@@ -404,6 +406,12 @@ void overlay::SpiceOverlay::init() {
window_fps->set_active(true); 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 // owned separately from `windows` so it is not part of the overlay window layer
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this); window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
@@ -556,13 +564,18 @@ void overlay::SpiceOverlay::new_frame() {
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
&& this->window_fps->get_active(); && 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 // draw RB touch diagnostics
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
&& games::rb::touch_debug_overlay_enabled(); && games::rb::touch_debug_overlay_enabled();
// check if there is nothing to draw // check if there is nothing to draw
this->has_pending_frame = false; 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; return;
} }
@@ -596,7 +609,13 @@ void overlay::SpiceOverlay::new_frame() {
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->build(); 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 // draw the main menu on top of the overlay windows
this->window_main_menu->build(); this->window_main_menu->build();
@@ -819,6 +838,10 @@ void overlay::SpiceOverlay::update() {
// FPS window // FPS window
this->window_fps->update(); 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) // main menu (owned separately from the overlay window layer)
this->window_main_menu->update(); this->window_main_menu->update();
+4
View File
@@ -83,6 +83,7 @@ namespace overlay {
// not part of `windows`: drawn/updated on the persistent layer (like // not part of `windows`: drawn/updated on the persistent layer (like
// notifications), independent of the overlay's active state and input gates. // notifications), independent of the overlay's active state and input gates.
std::unique_ptr<Window> window_fps; 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 // not part of `windows`: the main menu / launcher. owned and drawn
// separately from the overlay window layer; it drives the overlay's // separately from the overlay window layer; it drives the overlay's
@@ -138,6 +139,9 @@ namespace overlay {
inline IDirect3DDevice9 *get_device() { inline IDirect3DDevice9 *get_device() {
return this->device; return this->device;
} }
inline HWND get_window() {
return this->hWnd;
}
bool can_transform_touch_input() { bool can_transform_touch_input() {
return (this->subscreen_mouse_handler != nullptr); return (this->subscreen_mouse_handler != nullptr);
@@ -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;
};
}
@@ -2,6 +2,9 @@
// mingw otherwise doesn't load touch stuff // mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601 #define _WIN32_WINNT 0x0601
#include <atomic>
#include "nativetouchhook.h"
#include "avs/game.h" #include "avs/game.h"
#include "rawinput/touch.h" #include "rawinput/touch.h"
#include "inject.h" #include "inject.h"
@@ -32,6 +35,7 @@ namespace nativetouch {
static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr; static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr;
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
static std::atomic<TouchInputFilter> touch_input_filter { nullptr };
static bool native_touch_hooked = false; static bool native_touch_hooked = false;
static bool native_display_initialized = false; static bool native_display_initialized = false;
static DWORD native_display_orientation = DMDO_DEFAULT; static DWORD native_display_orientation = DMDO_DEFAULT;
@@ -184,6 +188,22 @@ namespace nativetouch {
point->dwFlags = 0; 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; return result;
@@ -193,6 +213,10 @@ namespace nativetouch {
return native_touch_hooked; return native_touch_hooked;
} }
void set_input_filter(TouchInputFilter filter) {
touch_input_filter.store(filter, std::memory_order_release);
}
void refresh_contact_lifetime() { void refresh_contact_lifetime() {
if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) { if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
inject::refresh_contact_lifetime(); inject::refresh_contact_lifetime();
@@ -1,8 +1,23 @@
#pragma once
#include <windows.h> #include <windows.h>
namespace nativetouch { 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 hook(HMODULE module);
void refresh_contact_lifetime(); void refresh_contact_lifetime();
void set_input_filter(TouchInputFilter filter);
// true once hook() has installed the native touch stack for the current game; // true once hook() has installed the native touch stack for the current game;
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch // such games consume touch through the GetTouchInputInfo hook rather than spicetouch