mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30: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:
@@ -412,6 +412,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/sdvx/io.cpp
|
||||
games/sdvx/camera.cpp
|
||||
games/jb/jb.cpp
|
||||
games/jb/jb_touch.cpp
|
||||
games/jb/io.cpp
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
|
||||
+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);
|
||||
}
|
||||
@@ -1314,10 +1314,24 @@ int main_implementation(int argc, char *argv[]) {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
|
||||
} else {
|
||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||
}
|
||||
}
|
||||
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
|
||||
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
|
||||
if (mode == "none") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
|
||||
} else if (mode == "box") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
|
||||
} else if (mode == "all") {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
|
||||
} else {
|
||||
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
|
||||
}
|
||||
}
|
||||
|
||||
// reflec beat touch emulation
|
||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||
|
||||
@@ -2691,6 +2691,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
|
||||
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
@@ -2698,10 +2699,32 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.elements = {
|
||||
{"legacy", ""},
|
||||
{"improved", ""},
|
||||
{"plus", ""},
|
||||
{"accurate", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// JubeatTouchDebug
|
||||
.title = "JB Touch Debug Overlay",
|
||||
.name = "jubeattouchdebug",
|
||||
.desc = "For touch screen players: draw a debug overlay on the main display. "
|
||||
"Requires the Spice Overlay to be enabled.\n\n"
|
||||
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
|
||||
"none - draw nothing\n\n"
|
||||
"box - show the 4x4 touch boundary boxes\n\n"
|
||||
"all - show both the boxes and the touch circles",
|
||||
.type = OptionType::Enum,
|
||||
.game_name = "Jubeat",
|
||||
.category = "Game Options",
|
||||
.elements = {
|
||||
{"auto", ""},
|
||||
{"none", ""},
|
||||
{"box", ""},
|
||||
{"all", ""},
|
||||
},
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// spice2x_RBTouchScale
|
||||
.title = "RB Touch Emulation Scale",
|
||||
|
||||
@@ -263,6 +263,7 @@ namespace launcher {
|
||||
IIDXWindowedSubscreenAlwaysOnTop,
|
||||
spice2x_JubeatLegacyTouch,
|
||||
JubeatTouchAlgo,
|
||||
JubeatTouchDebug,
|
||||
spice2x_RBTouchScale,
|
||||
RBTouchSize,
|
||||
RBTouchPollRate,
|
||||
|
||||
+34
-32
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -382,52 +383,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 29;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 21;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 17;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 22;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 18;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 23;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 19;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 26;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
|
||||
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
|
||||
|
||||
// update touch
|
||||
games::jb::touch_update();
|
||||
auto touched = games::jb::touch_state();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::jb::get_buttons();
|
||||
@@ -455,52 +457,52 @@ static void __cdecl device_update() {
|
||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 24;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 5;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 1;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 13;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 9;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 6;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 2;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 14;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 10;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 7;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 3;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 15;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 11;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 16;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 4;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 20;
|
||||
}
|
||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||
DEVICE_INPUT_STATE |= 1 << 12;
|
||||
}
|
||||
}
|
||||
|
||||
+49
-50
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "games/jb/jb.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
@@ -248,16 +249,15 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
if (!SPICETOUCH_REGISTERED_TOUCH) {
|
||||
SPICETOUCH_REGISTERED_TOUCH = true;
|
||||
|
||||
// check if touch is available
|
||||
// register the handler when a touch screen is present
|
||||
if (is_touch_available("SpiceTouchWndProc")) {
|
||||
|
||||
// notify the handler of our window
|
||||
TOUCH_HANDLER->window_register(hWnd);
|
||||
}
|
||||
|
||||
// enable card unless the feature is disabled
|
||||
if (!SPICETOUCH_CARD_DISABLE) {
|
||||
SPICETOUCH_CARD_ENABLED = true;
|
||||
}
|
||||
// enable the card button whenever the option is set, even without a touch
|
||||
// screen (mouse clicks are handled as touch input and can trigger it)
|
||||
if (!SPICETOUCH_CARD_DISABLE) {
|
||||
SPICETOUCH_CARD_ENABLED = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,52 +340,50 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
// draw overlay
|
||||
// render the software overlay to a bitmap BEFORE BeginPaint: the imgui
|
||||
// render is expensive, and running it between the background erase and the
|
||||
// blit leaves the window transparent long enough to flicker
|
||||
HBITMAP overlay_bitmap = nullptr;
|
||||
int overlay_width = 0, overlay_height = 0;
|
||||
if (overlay_enabled) {
|
||||
|
||||
// update and render
|
||||
overlay::OVERLAY->update();
|
||||
overlay::OVERLAY->new_frame();
|
||||
overlay::OVERLAY->render();
|
||||
|
||||
// get pixel data
|
||||
int width, height;
|
||||
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height);
|
||||
if (width > 0 && height > 0) {
|
||||
|
||||
// create bitmap
|
||||
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
|
||||
|
||||
// prepare paint
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
|
||||
/*
|
||||
* draw bitmap
|
||||
* - this currently sets the background to black because of SRCCOPY
|
||||
* - SRCPAINT will blend but colors are wrong
|
||||
* - once this is figured out we could also try hooking WM_PAINT and
|
||||
* draw directly to the game window
|
||||
*/
|
||||
SelectObject(hdcMem, bitmap);
|
||||
BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
|
||||
|
||||
// clean up
|
||||
DeleteObject(bitmap);
|
||||
DeleteDC(hdcMem);
|
||||
EndPaint(hWnd, &paint);
|
||||
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&overlay_width, &overlay_height);
|
||||
if (overlay_width > 0 && overlay_height > 0) {
|
||||
overlay_bitmap = CreateBitmap(overlay_width, overlay_height, 1, 8 * sizeof(uint32_t), pixel_data);
|
||||
}
|
||||
}
|
||||
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
|
||||
|
||||
// draw card input
|
||||
if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) {
|
||||
// draw everything in a single BeginPaint/EndPaint (a WM_PAINT has one update
|
||||
// region, so a second BeginPaint would get an empty region), keeping only
|
||||
// fast blits between the background erase and EndPaint
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
|
||||
// prepare paint
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
// blit the overlay bitmap
|
||||
if (overlay_bitmap) {
|
||||
|
||||
/*
|
||||
* draw bitmap
|
||||
* - this currently sets the background to black because of SRCCOPY
|
||||
* - SRCPAINT will blend but colors are wrong
|
||||
* - once this is figured out we could also try hooking WM_PAINT and
|
||||
* draw directly to the game window
|
||||
*/
|
||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
||||
SelectObject(hdcMem, overlay_bitmap);
|
||||
BitBlt(hdc, 0, 0, overlay_width, overlay_height, hdcMem, 0, 0, SRCCOPY);
|
||||
DeleteDC(hdcMem);
|
||||
DeleteObject(overlay_bitmap);
|
||||
}
|
||||
|
||||
// draw the insert-card button below the jubeat debug overlay; it is
|
||||
// hidden while the overlay is active
|
||||
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
|
||||
|
||||
// create brushes
|
||||
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
|
||||
@@ -443,14 +441,15 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
||||
// delete objects
|
||||
DeleteObject(brushFill);
|
||||
DeleteObject(brushBorder);
|
||||
|
||||
// end paint
|
||||
EndPaint(hWnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call default window procedure
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
// draw the jubeat debug overlay on top (hidden while the overlay is active)
|
||||
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
|
||||
games::jb::touch_draw_debug_overlay(hdc);
|
||||
}
|
||||
|
||||
EndPaint(hWnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
case WM_CREATE: {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user