mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20: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/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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ namespace launcher {
|
|||||||
DDRP4IOBufferMode,
|
DDRP4IOBufferMode,
|
||||||
InputRequiresFocus,
|
InputRequiresFocus,
|
||||||
NostalgiaPoke,
|
NostalgiaPoke,
|
||||||
|
NostalgiaTouchMode,
|
||||||
ForceBackBufferCount,
|
ForceBackBufferCount,
|
||||||
SDVXWindowedSubscreenSize,
|
SDVXWindowedSubscreenSize,
|
||||||
SDVXWindowedSubscreenPosition,
|
SDVXWindowedSubscreenPosition,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user