From eb037542b4033aef21ae1579d779f0ae455ed585 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 9 May 2026 04:22:29 -0700 Subject: [PATCH] jubeat: introduce new algorithm for touch (#686) ## Description of change There was enough feedback that the current algorithm is not ideal for most touch screens because most touch screens are not 24". In the current algorithm touching the gap is no-op, so people think the game is dropping touches. This PR introduces a new algorithm that inspect touches on the gap and triggers the nearest square. ## Testing Tested landscape and portrait jubeat. --- src/spice2x/games/jb/jb.cpp | 95 +++++++++++++++++++++++++++---- src/spice2x/games/jb/jb.h | 8 ++- src/spice2x/launcher/launcher.cpp | 12 +++- src/spice2x/launcher/options.cpp | 22 ++++++- src/spice2x/launcher/options.h | 1 + 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/src/spice2x/games/jb/jb.cpp b/src/spice2x/games/jb/jb.cpp index 46a39a2..c3bf1ca 100644 --- a/src/spice2x/games/jb/jb.cpp +++ b/src/spice2x/games/jb/jb.cpp @@ -19,7 +19,7 @@ namespace games::jb { // touch stuff - bool TOUCH_LEGACY_BOX = false; + JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved; static bool TOUCH_ENABLE = false; static bool TOUCH_ATTACHED = false; static bool IS_PORTRAIT = true; @@ -78,7 +78,7 @@ namespace games::jb { // 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_LEGACY_BOX) { + if (TOUCH_ALGORITHM == Legacy) { auto offset = IS_PORTRAIT ? 580 : 0; for (auto &tp : TOUCH_POINTS) { @@ -94,6 +94,17 @@ namespace games::jb { } } 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; @@ -110,19 +121,66 @@ namespace games::jb { y_relative -= 8; } - if (x_relative < 0 || y_relative < 0) { + 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; } - // x_hitbox and y_hitbox is relative to top-left pixel of each button - int x_index = x_relative / JB_BUTTON_HITBOX; - int x_hitbox = x_relative % JB_BUTTON_HITBOX; - int y_index = y_relative / JB_BUTTON_HITBOX; - int y_hitbox = y_relative % JB_BUTTON_HITBOX; - // check if the gap was touched - if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) { - continue; + 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 @@ -193,6 +251,21 @@ namespace games::jb { // 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; + } + // enable debug logging of gftools HMODULE gftools = libutils::try_module("gftools.dll"); detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( diff --git a/src/spice2x/games/jb/jb.h b/src/spice2x/games/jb/jb.h index 18425bf..08c69a8 100644 --- a/src/spice2x/games/jb/jb.h +++ b/src/spice2x/games/jb/jb.h @@ -4,8 +4,14 @@ namespace games::jb { + enum JubeatTouchAlgorithm { + Legacy, + Improved, + AcAccurate + }; + // touch stuff - extern bool TOUCH_LEGACY_BOX; + extern JubeatTouchAlgorithm TOUCH_ALGORITHM; extern bool TOUCH_STATE[16]; void touch_update(); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 57ca956..56c6ab6 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1148,8 +1148,18 @@ int main_implementation(int argc, char *argv[]) { } if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) { - games::jb::TOUCH_LEGACY_BOX = true; + games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy; } + if (options[launcher::Options::JubeatTouchAlgo].is_active()) { + if (options[launcher::Options::JubeatTouchAlgo].value_text() == "accurate") { + 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 { + games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved; + } + } + if (options[launcher::Options::spice2x_RBTouchScale].is_active()) { games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 43b3175..fb35edf 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2279,16 +2279,32 @@ static const std::vector OPTION_DEFINITIONS = { }, { // spice2x_JubeatLegacyTouch - .title = "JB Legacy Touch Targets", + .title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)", .name = "sp2x-jubeatlegacytouch", .display_name = "jubeatlegacytouch", .aliases= "jubeatlegacytouch", - .desc = "For touch screen players - use the legacy & less accurate grid-based layout for touch recognition, " - "instead of the new & more accurate touch targets. Default: off.", + .desc = "Deprecated - use -jubeattouchalgo instead.", .type = OptionType::Bool, + .hidden = true, .game_name = "Jubeat", .category = "Game Options", }, + { + .title = "JB Touch Algorithm", + .name = "jubeattouchalgo", + .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" + "accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)", + .type = OptionType::Enum, + .game_name = "Jubeat", + .category = "Game Options", + .elements = { + {"legacy", ""}, + {"improved", ""}, + {"accurate", ""}, + }, + }, { // spice2x_RBTouchScale .title = "RB Scale Touch Input", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index e023cc3..5e6e160 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -239,6 +239,7 @@ namespace launcher { IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenAlwaysOnTop, spice2x_JubeatLegacyTouch, + JubeatTouchAlgo, spice2x_RBTouchScale, spice2x_AsioForceUnload, spice2x_IIDXNoESpec,