mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace games::nost {
|
||||
|
||||
extern bool ENABLE_POKE;
|
||||
extern bool ENABLE_TOUCH_MODE;
|
||||
|
||||
class NostGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3149,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
|
||||
|
||||
@@ -297,6 +297,7 @@ namespace launcher {
|
||||
DDRP4IOBufferMode,
|
||||
InputRequiresFocus,
|
||||
NostalgiaPoke,
|
||||
NostalgiaTouchMode,
|
||||
ForceBackBufferCount,
|
||||
SDVXWindowedSubscreenSize,
|
||||
SDVXWindowedSubscreenPosition,
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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> 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#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<TouchInputFilter> 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();
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user