mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-04 07:40:42 -07:00
jb: option to draw debug graphics to show touch interactions (#797)
## Link to GitHub Issue or related Pull Request, if one exists related to #697 ## Description of change Add a new option that paints touch targets and touch points for jubeat; gets enabled if there is a touch screen connected. This is to help explain / debug issues when users have their touch screen issues, which is usually due to misalignment. Fix the touch targets being off by 1 pixel (center gap is 1px wider in both directions). Also, simplify the `improved` algorithm to simulate a finger and trigger nearest-neighbor for the AC dimensions. In terms of touch performance this is near identical to what we had before, but it allows us to draw a nice debug overlay instead of just grids. ## Testing Tested portrait and landscape versions of jb.
This commit is contained in:
+5
-214
@@ -3,216 +3,24 @@
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "touch/touch.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_BUTTON_GAP 37
|
||||
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch stuff
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
static bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::vector<TouchPoint> TOUCH_POINTS;
|
||||
bool TOUCH_STATE[16];
|
||||
|
||||
void touch_update() {
|
||||
|
||||
// check if touch enabled
|
||||
if (!TOUCH_ENABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch module
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
/*
|
||||
* Find the game window.
|
||||
* We check the foreground window first, then fall back to searching for the window title
|
||||
* All game versions seem to have their model first in the window title
|
||||
*/
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
|
||||
// check if we have a window handle
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// attach touch hook
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
// show cursor
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
// earlier games use a different screen orientation
|
||||
if (!avs::game::is_model("L44")) {
|
||||
IS_PORTRAIT = false;
|
||||
}
|
||||
|
||||
// set attached
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// reset touch state
|
||||
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
|
||||
|
||||
// check touch points
|
||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
||||
TOUCH_POINTS.clear();
|
||||
touch_get_points(TOUCH_POINTS);
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
|
||||
// get grid coordinates
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
|
||||
// set the corresponding state
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
// check window out of bounds
|
||||
if (IS_PORTRAIT) {
|
||||
if (tp.x > 768 || tp.y > 1360) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (tp.x > 1360 || tp.y > 768) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int x_relative = tp.x;
|
||||
int y_relative = tp.y;
|
||||
|
||||
// x_relative and y_relative are relative to the top-left pixel of the first button
|
||||
if (IS_PORTRAIT) {
|
||||
// which is at (8, 602) in portrait:
|
||||
// X: [8...759] (752 pixels wide)
|
||||
// Y: [602...1353] (752 pixels high)
|
||||
x_relative -= 8;
|
||||
y_relative -= 602;
|
||||
} else {
|
||||
// and at (8, 8) in landscape
|
||||
x_relative -= 8;
|
||||
y_relative -= 8;
|
||||
}
|
||||
|
||||
int x_index = -1;
|
||||
int x_hitbox = 0;
|
||||
int y_index = -1;
|
||||
int y_hitbox = 0;
|
||||
|
||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
||||
if (x_relative >= 0) {
|
||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
if (y_relative >= 0) {
|
||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
|
||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
||||
|
||||
if (TOUCH_ALGORITHM == Improved) {
|
||||
if (IS_PORTRAIT) {
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// right and bottom borders are covered by the hit box
|
||||
} else {
|
||||
// extend to top border
|
||||
if (y_relative < 0) {
|
||||
y_index = 0;
|
||||
}
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// bottom border is covered by the hit box
|
||||
// rightmost edge - ignore them entirely
|
||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the gap was touched
|
||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
||||
// in ac-accurate mode, touching the gap is ignored
|
||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (TOUCH_ALGORITHM == Improved) {
|
||||
// in improved mode, touching the gap triggers the closest button
|
||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
||||
x_index++;
|
||||
}
|
||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
||||
y_index++;
|
||||
}
|
||||
}
|
||||
|
||||
// set the corresponding state
|
||||
int index = y_index * 4 + x_index;
|
||||
if (0 <= index && index < 16) {
|
||||
TOUCH_STATE[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
|
||||
*/
|
||||
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||
static BOOL __stdcall network_addr_is_changed() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
|
||||
*/
|
||||
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
|
||||
static BOOL __stdcall network_get_network_check_info() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* to fix network error on non DHCP interfaces
|
||||
*/
|
||||
// fixes network errors on non-DHCP interfaces
|
||||
static BOOL __cdecl network_get_dhcp_result() {
|
||||
return 1;
|
||||
}
|
||||
@@ -252,23 +60,7 @@ namespace games::jb {
|
||||
void JBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
// enable touch
|
||||
TOUCH_ENABLE = true;
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
||||
break;
|
||||
}
|
||||
touch_attach();
|
||||
|
||||
// enable debug logging of gftools
|
||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||
@@ -288,7 +80,6 @@ namespace games::jb {
|
||||
void JBGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
// disable touch
|
||||
TOUCH_ENABLE = false;
|
||||
touch_detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "games/game.h"
|
||||
#include "games/jb/jb_touch.h"
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// touch stuff
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern bool TOUCH_STATE[16];
|
||||
void touch_update();
|
||||
|
||||
class JBGame : public games::Game {
|
||||
public:
|
||||
JBGame();
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
#include "jb_touch.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
|
||||
// (752px across). the first button's top-left is at (8, 602) in portrait and
|
||||
// (6, 8) in landscape.
|
||||
#define JB_BUTTON_SIZE 160
|
||||
#define JB_MAX_BUTTON_GAP 38
|
||||
|
||||
// improved and plus modes use this reach around each button. must be >= the
|
||||
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||
#define JB_TOUCH_RADIUS 38
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
static_assert(std::atomic_bool::is_always_lock_free);
|
||||
static_assert(std::atomic_uint16_t::is_always_lock_free);
|
||||
static_assert(std::atomic<POINT>::is_always_lock_free);
|
||||
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
|
||||
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
|
||||
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
|
||||
|
||||
// touch state
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
|
||||
static std::atomic_bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||
|
||||
// fixed-size contact view used by the debug overlay
|
||||
static const size_t JB_MAX_TOUCH_POINTS = 16;
|
||||
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
|
||||
static constexpr POINT JB_INVALID_TOUCH_POINT {
|
||||
JB_INVALID_TOUCH_COORD,
|
||||
JB_INVALID_TOUCH_COORD
|
||||
};
|
||||
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
|
||||
|
||||
static void clear_touch_points() {
|
||||
for (auto &point : TOUCH_POINTS) {
|
||||
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
static void publish_touch_points(const std::vector<TouchPoint> &touch_points) {
|
||||
size_t count = touch_points.size();
|
||||
if (count > JB_MAX_TOUCH_POINTS) {
|
||||
count = JB_MAX_TOUCH_POINTS;
|
||||
}
|
||||
|
||||
for (size_t i = count; i < JB_MAX_TOUCH_POINTS; i++) {
|
||||
TOUCH_POINTS[i].store(JB_INVALID_TOUCH_POINT, std::memory_order_relaxed);
|
||||
}
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
POINT point { touch_points[i].x, touch_points[i].y };
|
||||
TOUCH_POINTS[i].store(point, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// --- touch geometry ------------------------------------------------------
|
||||
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||
|
||||
struct AxisGeometry {
|
||||
int button[4]; // left/top edge of each button
|
||||
};
|
||||
|
||||
// left/top edges of the four buttons along one axis, starting at `first`
|
||||
static AxisGeometry axis_geometry(int first) {
|
||||
AxisGeometry g {};
|
||||
g.button[0] = first;
|
||||
for (int i = 1; i < 4; i++) {
|
||||
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// button edges for the current orientation
|
||||
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
||||
if (IS_PORTRAIT) {
|
||||
gx = axis_geometry(8);
|
||||
gy = axis_geometry(602);
|
||||
} else {
|
||||
gx = axis_geometry(6);
|
||||
gy = axis_geometry(8);
|
||||
}
|
||||
}
|
||||
|
||||
// distance from `p` to a button along one axis (0 when inside)
|
||||
static int axis_distance(int p, int button) {
|
||||
int end = button + JB_BUTTON_SIZE - 1;
|
||||
if (p < button) {
|
||||
return button - p;
|
||||
}
|
||||
if (p > end) {
|
||||
return p - end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
|
||||
// shared by detection and the debug overlay so both agree which button a touch hits
|
||||
static int nearest_button(
|
||||
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
|
||||
int best_index = -1;
|
||||
int best_dist = 0;
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
int dist = dx * dx + dy * dy;
|
||||
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||
best_dist = dist;
|
||||
best_index = r * 4 + c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
// detection reach for the current algorithm (0 = register only inside a button)
|
||||
static int touch_radius() {
|
||||
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
|
||||
}
|
||||
|
||||
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||
// every button within `radius` when `multi` (edge/gap presses trigger several)
|
||||
static void mark_buttons(uint16_t &state, int px, int py,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
int radius, bool multi) {
|
||||
if (!multi) {
|
||||
int index = nearest_button(px, py, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
state |= uint16_t(1) << index;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
if (dx * dx + dy * dy <= radius * radius) {
|
||||
state |= uint16_t(1) << (r * 4 + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::bitset<16> touch_state() {
|
||||
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
void touch_update() {
|
||||
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// one-time touch window attach
|
||||
if (!TOUCH_ATTACHED) {
|
||||
|
||||
// find the game window: prefer the foreground window, else search by
|
||||
// title (the model name prefixes the window title in every version)
|
||||
HWND wnd = GetForegroundWindow();
|
||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
}
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
// only the L44 model runs in portrait; set this before starting the
|
||||
// touch-window thread so the renderer only observes the final value
|
||||
IS_PORTRAIT = avs::game::is_model("L44");
|
||||
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// let the rawinput stack correct the aspect ratio
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
|
||||
TOUCH_ATTACHED = true;
|
||||
}
|
||||
|
||||
// calculate the next state locally and publish it after processing every touch
|
||||
uint16_t next_state = 0;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
touch_get_points(touch_points);
|
||||
publish_touch_points(touch_points);
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
|
||||
// legacy: evenly divide the play area into a 4x4 grid
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : touch_points) {
|
||||
int x = tp.x * 4 / 768;
|
||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||
int index = y * 4 + x;
|
||||
if (index >= 0 && index < 16) {
|
||||
next_state |= uint16_t(1) << index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// accurate registers only a touch inside a button; improved snaps each touch
|
||||
// to the single nearest button within reach (so a gap or centre touch still
|
||||
// triggers exactly one button); plus marks every button within the same reach,
|
||||
// so an edge or gap touch can trigger several at once (like the mobile game)
|
||||
AxisGeometry gx, gy;
|
||||
touch_geometry(gx, gy);
|
||||
int radius = touch_radius();
|
||||
bool multi = (TOUCH_ALGORITHM == Plus);
|
||||
for (auto &tp : touch_points) {
|
||||
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
|
||||
}
|
||||
}
|
||||
|
||||
TOUCH_STATE.store(next_state, std::memory_order_release);
|
||||
}
|
||||
|
||||
// true when any supported touch handler detects a touchscreen (checked once)
|
||||
static bool touchscreen_detected() {
|
||||
static const bool detected = is_touch_available("jubeat touch debug overlay");
|
||||
return detected;
|
||||
}
|
||||
|
||||
// auto draws the boxes only when a touch screen is present
|
||||
static bool debug_show_boxes() {
|
||||
switch (TOUCH_DEBUG_OVERLAY) {
|
||||
case JbTouchDebugBox:
|
||||
case JbTouchDebugAll:
|
||||
return true;
|
||||
case JbTouchDebugAuto:
|
||||
return touchscreen_detected();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool debug_show_taps() {
|
||||
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
|
||||
}
|
||||
|
||||
bool touch_debug_overlay_enabled() {
|
||||
return debug_show_boxes() || debug_show_taps();
|
||||
}
|
||||
|
||||
// the 16 boundary rects: legacy divides the area evenly, others use button squares
|
||||
static void debug_cells(
|
||||
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
|
||||
if (legacy) {
|
||||
int offset = IS_PORTRAIT ? 580 : 0;
|
||||
int x_edges[5];
|
||||
int y_edges[5];
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
x_edges[i] = i * 768 / 4;
|
||||
y_edges[i] = offset + i * (1360 - 580) / 4;
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = {
|
||||
gx.button[c], gy.button[r],
|
||||
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
|
||||
static void draw_debug_boxes(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
RECT cells[16];
|
||||
debug_cells(legacy, gx, gy, cells);
|
||||
|
||||
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
|
||||
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
|
||||
auto state = touch_state();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
|
||||
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
|
||||
SelectObject(hdc, old_pen);
|
||||
}
|
||||
DeleteObject(pen_idle);
|
||||
DeleteObject(pen_active);
|
||||
}
|
||||
|
||||
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
|
||||
// reaches across gaps to nearby buttons, and the others only fire inside a button square
|
||||
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
|
||||
bool legacy, int radius) {
|
||||
if (legacy) {
|
||||
return true;
|
||||
}
|
||||
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
|
||||
return nearest_button(px, py, gx, gy, reach) >= 0;
|
||||
}
|
||||
|
||||
// 90-degree arc on the circle rim facing the centre of button `index`
|
||||
static void draw_tap_arc(HDC hdc, int px, int py, int index,
|
||||
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||
int c = index % 4;
|
||||
int r = index / 4;
|
||||
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
||||
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
||||
const double quarter = 3.14159265358979323846 / 2.0;
|
||||
const int segments = 16;
|
||||
POINT arc[segments + 1];
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
double a = mid - quarter / 2.0 + quarter * i / segments;
|
||||
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
|
||||
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
|
||||
}
|
||||
Polyline(hdc, arc, segments + 1);
|
||||
}
|
||||
|
||||
// circle at each touch: white when it presses a button, grey otherwise; in single-button
|
||||
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
|
||||
// plus, where a touch can trigger several buttons at once)
|
||||
static void draw_debug_taps(
|
||||
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||
// PS_INSIDEFRAME keeps the stroke inside the circle radius
|
||||
const int stroke = 4;
|
||||
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
|
||||
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
|
||||
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
|
||||
|
||||
// accurate has no reach, so fall back to a visible marker size when drawing the circle
|
||||
int radius = touch_radius();
|
||||
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
|
||||
|
||||
for (auto &touch_point : TOUCH_POINTS) {
|
||||
POINT point = touch_point.load(std::memory_order_relaxed);
|
||||
if (point.x == JB_INVALID_TOUCH_COORD) {
|
||||
continue;
|
||||
}
|
||||
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
|
||||
|
||||
SelectObject(hdc, pressed ? pen_white : pen_gray);
|
||||
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
|
||||
point.x + draw_radius, point.y + draw_radius);
|
||||
|
||||
// the arc would point at a single snapped-to button; plus can trigger several at
|
||||
// once, so skip it there
|
||||
if (pressed || TOUCH_ALGORITHM == Plus) {
|
||||
continue;
|
||||
}
|
||||
int index = nearest_button(point.x, point.y, gx, gy, radius);
|
||||
if (index >= 0) {
|
||||
SelectObject(hdc, pen_white);
|
||||
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SelectObject(hdc, old_pen);
|
||||
DeleteObject(pen_white);
|
||||
DeleteObject(pen_gray);
|
||||
}
|
||||
|
||||
void touch_draw_debug_overlay(HDC hdc) {
|
||||
|
||||
// only draw while touch is active
|
||||
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool show_boxes = debug_show_boxes();
|
||||
bool show_taps = debug_show_taps();
|
||||
if (!show_boxes && !show_taps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// legacy divides the field evenly; the other algorithms use button squares
|
||||
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
||||
AxisGeometry gx {}, gy {};
|
||||
if (!legacy) {
|
||||
touch_geometry(gx, gy);
|
||||
}
|
||||
|
||||
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
|
||||
if (show_boxes) {
|
||||
draw_debug_boxes(hdc, legacy, gx, gy);
|
||||
}
|
||||
if (show_taps) {
|
||||
draw_debug_taps(hdc, legacy, gx, gy);
|
||||
}
|
||||
SelectObject(hdc, old_brush);
|
||||
}
|
||||
|
||||
void touch_attach() {
|
||||
clear_touch_points();
|
||||
TOUCH_ENABLE.store(true, std::memory_order_release);
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case Plus:
|
||||
log_info("jubeat", "using 'plus' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo, this is a bug");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void touch_detach() {
|
||||
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||
TOUCH_STATE.store(0, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
// touch detection algorithm
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
Plus,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// debug touch overlay mode
|
||||
enum JubeatTouchDebugMode {
|
||||
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
|
||||
JbTouchDebugNone, // draw nothing
|
||||
JbTouchDebugBox, // boundary boxes
|
||||
JbTouchDebugAll, // boundary boxes and touch circles
|
||||
};
|
||||
|
||||
// selected algorithm and debug mode (set from the launcher options)
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
|
||||
|
||||
// atomically published panel state, read by the I/O layer
|
||||
std::bitset<16> touch_state();
|
||||
|
||||
// read the current touch points and refresh the panel state
|
||||
void touch_update();
|
||||
|
||||
// enable/disable touch handling on game attach/detach
|
||||
void touch_attach();
|
||||
void touch_detach();
|
||||
|
||||
// whether the debug overlay should currently draw anything
|
||||
bool touch_debug_overlay_enabled();
|
||||
|
||||
// draw the debug touch overlay (boundary boxes and/or touch circles per the
|
||||
// selected mode) onto the spice touch overlay window's device context
|
||||
void touch_draw_debug_overlay(HDC hdc);
|
||||
}
|
||||
Reference in New Issue
Block a user