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.
This commit is contained in:
bicarus
2026-05-09 04:22:29 -07:00
committed by GitHub
parent 039b42ad53
commit eb037542b4
5 changed files with 122 additions and 16 deletions
+84 -11
View File
@@ -19,7 +19,7 @@
namespace games::jb { namespace games::jb {
// touch stuff // touch stuff
bool TOUCH_LEGACY_BOX = false; JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false; static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false; static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true; 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. // note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear(); TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS); touch_get_points(TOUCH_POINTS);
if (TOUCH_LEGACY_BOX) { if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0; auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -94,6 +94,17 @@ namespace games::jb {
} }
} else { } else {
for (auto &tp : TOUCH_POINTS) { 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 x_relative = tp.x;
int y_relative = tp.y; int y_relative = tp.y;
@@ -110,19 +121,66 @@ namespace games::jb {
y_relative -= 8; 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; 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 // check if the gap was touched
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) { if (TOUCH_ALGORITHM == AcAccurate) {
continue; // 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 // set the corresponding state
@@ -193,6 +251,21 @@ namespace games::jb {
// enable touch // enable touch
TOUCH_ENABLE = true; 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 // enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll"); HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
+7 -1
View File
@@ -4,8 +4,14 @@
namespace games::jb { namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff // touch stuff
extern bool TOUCH_LEGACY_BOX; extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16]; extern bool TOUCH_STATE[16];
void touch_update(); void touch_update();
+11 -1
View File
@@ -1148,8 +1148,18 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) { 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()) { if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
} }
+19 -3
View File
@@ -2279,16 +2279,32 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_JubeatLegacyTouch // spice2x_JubeatLegacyTouch
.title = "JB Legacy Touch Targets", .title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)",
.name = "sp2x-jubeatlegacytouch", .name = "sp2x-jubeatlegacytouch",
.display_name = "jubeatlegacytouch", .display_name = "jubeatlegacytouch",
.aliases= "jubeatlegacytouch", .aliases= "jubeatlegacytouch",
.desc = "For touch screen players - use the legacy & less accurate grid-based layout for touch recognition, " .desc = "Deprecated - use -jubeattouchalgo instead.",
"instead of the new & more accurate touch targets. Default: off.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true,
.game_name = "Jubeat", .game_name = "Jubeat",
.category = "Game Options", .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 // spice2x_RBTouchScale
.title = "RB Scale Touch Input", .title = "RB Scale Touch Input",
+1
View File
@@ -239,6 +239,7 @@ namespace launcher {
IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenBorderless,
IIDXWindowedSubscreenAlwaysOnTop, IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch, spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
spice2x_RBTouchScale, spice2x_RBTouchScale,
spice2x_AsioForceUnload, spice2x_AsioForceUnload,
spice2x_IIDXNoESpec, spice2x_IIDXNoESpec,