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
+24 -1
View File
@@ -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();
+4
View File
@@ -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;
};
}