gitadora: option for guitar picking algorithm (#528)

## Link to GitHub Issue, if one exists
n/a

## Description of change
Adds an option to pick from four algorithms for guitar picking input.

The default behavior is being switched from legacy (rising edge only,
always 1 frame of input) to SOCD "prefer recent" algorithm. This makes
it slightly easier to navigate the menu with the guitar.

## Testing
Validated gw, gwdelta, and sdvx just in case.
This commit is contained in:
bicarus
2026-01-21 21:14:28 -08:00
committed by GitHub
parent 1dd99aac74
commit 9fd9290cd3
9 changed files with 142 additions and 42 deletions
+9 -1
View File
@@ -24,6 +24,7 @@ namespace games::gitadora {
bool P1_LEFTY = false; bool P1_LEFTY = false;
bool P2_LEFTY = false; bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -227,8 +228,15 @@ namespace games::gitadora {
overlay::UI_SCALE_PERCENT = 250; overlay::UI_SCALE_PERCENT = 250;
} }
// for guitar wail SOCD cleaning // for guitar wail SOCD cleaning
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
// for guitar picking
if (PICK_ALGO.has_value()) {
log_info("gitadora", "guitar pick SOCD algorithm: {}", static_cast<int>(PICK_ALGO.value()));
} else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy");
}
} }
} }
+2
View File
@@ -4,6 +4,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/socd_cleaner.h"
namespace games::gitadora { namespace games::gitadora {
@@ -14,6 +15,7 @@ namespace games::gitadora {
extern bool P1_LEFTY; extern bool P1_LEFTY;
extern bool P2_LEFTY; extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
+34 -12
View File
@@ -92,22 +92,44 @@ bool games::gitadora::J33ISerialDevice::parse_msg(
payload.buttons |= 1 << GUITAR_BTN_P; payload.buttons |= 1 << GUITAR_BTN_P;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::GuitarP1PickUp])) {
if (!GFDM_GF_PICK_STATE_UP) { const auto pick_up = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::GuitarP1PickUp]);
GFDM_GF_PICK_STATE_UP = true; const auto pick_down = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::GuitarP1PickDown]);
payload.buttons |= 1 << GUITAR_PICK_UP; if (!games::gitadora::PICK_ALGO.has_value()) {
// legacy behavior (detect rising edges only, input for 1 frame)
if (pick_up) {
if (!GFDM_GF_PICK_STATE_UP) {
GFDM_GF_PICK_STATE_UP = true;
payload.buttons |= 1 << GUITAR_PICK_UP;
}
} else {
GFDM_GF_PICK_STATE_UP = false;
}
if (pick_down) {
if (!GFDM_GF_PICK_STATE_DOWN) {
GFDM_GF_PICK_STATE_DOWN = true;
payload.buttons |= 1 << GUITAR_PICK_DOWN;
}
} else {
GFDM_GF_PICK_STATE_DOWN = false;
} }
} else { } else {
GFDM_GF_PICK_STATE_UP = false; const auto socd = socd::socd_clean(
} 0,
pick_up,
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::GuitarP1PickDown])) { pick_down,
if (!GFDM_GF_PICK_STATE_DOWN) { get_performance_milliseconds(),
GFDM_GF_PICK_STATE_DOWN = true; games::gitadora::PICK_ALGO.value()
);
if (socd == socd::SocdCCW) {
payload.buttons |= 1 << GUITAR_PICK_UP;
} else if (socd == socd::SocdCW) {
payload.buttons |= 1 << GUITAR_PICK_DOWN;
} else if (socd == socd::SocdBoth) {
payload.buttons |= 1 << GUITAR_PICK_UP;
payload.buttons |= 1 << GUITAR_PICK_DOWN; payload.buttons |= 1 << GUITAR_PICK_DOWN;
} }
} else {
GFDM_GF_PICK_STATE_DOWN = false;
} }
auto &analogs = get_analogs(); auto &analogs = get_analogs();
+10
View File
@@ -595,6 +595,16 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraWailHold].is_active()) { if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32(); socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
} }
if (options[launcher::Options::GitaDoraPickAlgo].is_active()) {
const auto text = options[launcher::Options::GitaDoraPickAlgo].value_text();
if (text == "legacy") {
games::gitadora::PICK_ALGO.reset();
} else if (text == "neutral") {
games::gitadora::PICK_ALGO = socd::SocdAlgorithm::Neutral;
} else if (text == "raw") {
games::gitadora::PICK_ALGO = socd::SocdAlgorithm::None;
}
}
if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) { if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) {
games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text(); games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text();
} }
+19
View File
@@ -1020,6 +1020,25 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "GitaDora", .game_name = "GitaDora",
.category = "Game Options", .category = "Game Options",
}, },
{
// GitaDoraPickAlgo
.title = "GitaDora Picking Algorithm",
.name = "gdpickalgo",
.desc = "Select picking algorithm for guitar.\n\n"
"recent (default): use SOCD cleaner algorithm that prioritizes recent input\n\n"
"legacy: only the rising edge of the pick input is considered; cannot hold pick for menu navigation\n\n"
"neutral: if both up and down are pressed, they cancel out\n\n"
"raw: no filtering done by spice; game receives both input as-is, possible to input both up and down at the same time",
.type = OptionType::Enum,
.game_name = "GitaDora",
.category = "Game Options",
.elements = {
{"recent", ""},
{"legacy", ""},
{"neutral", ""},
{"raw", ""}
},
},
{ {
// GitaDoraSubOverlaySize // GitaDoraSubOverlaySize
.title = "GitaDora Subscreen Overlay Size", .title = "GitaDora Subscreen Overlay Size",
+1
View File
@@ -103,6 +103,7 @@ namespace launcher {
GitaDoraArenaSingleWindow, GitaDoraArenaSingleWindow,
GitaDoraLefty, GitaDoraLefty,
GitaDoraWailHold, GitaDoraWailHold,
GitaDoraPickAlgo,
GitaDoraSubOverlaySize, GitaDoraSubOverlaySize,
LoadJubeatModule, LoadJubeatModule,
LoadReflecBeatModule, LoadReflecBeatModule,
+32 -14
View File
@@ -522,24 +522,42 @@ static long __cdecl gfdm_unit_get_input_p(int device, size_t player) {
if (games::gitadora::is_guitar()) { if (games::gitadora::is_guitar()) {
auto offset = player * 11; auto offset = player * 11;
// pick up const auto pick_up = Buttons::getState(RI_MGR, buttons.at(gitadora_button_mapping[3 + offset]));
if (Buttons::getState(RI_MGR, buttons.at(gitadora_button_mapping[3 + offset]))) { const auto pick_down = Buttons::getState(RI_MGR, buttons.at(gitadora_button_mapping[4 + offset]));
if (!GFDM_GF_PICK_STATE_UP[player]) { if (!games::gitadora::PICK_ALGO.has_value()) {
GFDM_GF_PICK_STATE_UP[player] = true; // legacy behavior (detect rising edges only, input for 1 frame)
ret |= 0x80 | 0x20; if (pick_up) {
if (!GFDM_GF_PICK_STATE_UP[player]) {
GFDM_GF_PICK_STATE_UP[player] = true;
ret |= 0x80 | 0x20;
}
} else {
GFDM_GF_PICK_STATE_UP[player] = false;
} }
} else {
GFDM_GF_PICK_STATE_UP[player] = false;
}
// pick down if (pick_down) {
if (Buttons::getState(RI_MGR, buttons.at(gitadora_button_mapping[4 + offset]))) { if (!GFDM_GF_PICK_STATE_DOWN[player]) {
if (!GFDM_GF_PICK_STATE_DOWN[player]) { GFDM_GF_PICK_STATE_DOWN[player] = true;
GFDM_GF_PICK_STATE_DOWN[player] = true; ret |= 0x100 | 0x20;
ret |= 0x100 | 0x20; }
} else {
GFDM_GF_PICK_STATE_DOWN[player] = false;
} }
} else { } else {
GFDM_GF_PICK_STATE_DOWN[player] = false; const auto socd = socd::socd_clean(
player,
pick_up,
pick_down,
get_performance_milliseconds(),
games::gitadora::PICK_ALGO.value()
);
if (socd == socd::SocdCCW) {
ret |= 0x80 | 0x20; // pick up
} else if (socd == socd::SocdCW) {
ret |= 0x100 | 0x20; // pick down
} else if (socd == socd::SocdBoth) {
ret |= 0x100 | 0x80 | 0x20; // up and down
}
} }
// button R // button R
+24 -12
View File
@@ -15,18 +15,23 @@ namespace socd {
SocdAlgorithm ALGORITHM = SocdAlgorithm::Neutral; SocdAlgorithm ALGORITHM = SocdAlgorithm::Neutral;
static double last_rising_edge[2][2] = {}; static double last_rising_edge[4][2] = {};
static bool last_button_state[2][2] = {}; static bool last_button_state[4][2] = {};
// this has no locking, use only if you know there is only one i/o thread that will call this // this has no locking, use only if you know there is only one i/o thread that will call this
SocdResult socd_clean(uint8_t device, bool ccw, bool cw, double time_now) { SocdResult socd_clean(
if (device >= 2) { uint8_t device, bool ccw, bool cw, double time_now,
std::optional<SocdAlgorithm> algorithm_override) {
if (device >= 4) {
log_fatal("socd", "invalid device index in socd_clean: {}", device); log_fatal("socd", "invalid device index in socd_clean: {}", device);
} }
// SOCD last input algorithm needs to keep track of rising edge times SocdAlgorithm algo =
if (ALGORITHM != SocdAlgorithm::Neutral) { algorithm_override.has_value() ? algorithm_override.value() : ALGORITHM;
// detect rising edges
// keep track of rising edge times
if (algo == SocdAlgorithm::PreferRecent || algo == SocdAlgorithm::PreferFirst) {
if (!last_button_state[device][SocdCCW] && ccw) { if (!last_button_state[device][SocdCCW] && ccw) {
last_rising_edge[device][SocdCCW] = time_now; last_rising_edge[device][SocdCCW] = time_now;
} }
@@ -55,7 +60,7 @@ namespace socd {
const auto cw_time = last_rising_edge[device][SocdCW]; const auto cw_time = last_rising_edge[device][SocdCW];
log_debug("socd", "ccw={}, cw ={}", ccw_time, cw_time); log_debug("socd", "ccw={}, cw ={}", ccw_time, cw_time);
if (ALGORITHM == SocdAlgorithm::PreferRecent) { if (algo == SocdAlgorithm::PreferRecent) {
// SOCD: prefer last input // SOCD: prefer last input
if (ccw_time < cw_time) { if (ccw_time < cw_time) {
// while CCW is being held, CW got pressed // while CCW is being held, CW got pressed
@@ -67,7 +72,7 @@ namespace socd {
// it's a tie; instead of none, we'll pick a direction // it's a tie; instead of none, we'll pick a direction
return SocdCW; return SocdCW;
} }
} else if (ALGORITHM == SocdAlgorithm::PreferFirst) { } else if (algo == SocdAlgorithm::PreferFirst) {
// SOCD: keep first input // SOCD: keep first input
if (ccw_time < cw_time) { if (ccw_time < cw_time) {
// while CCW is being held, CW got pressed // while CCW is being held, CW got pressed
@@ -79,9 +84,15 @@ namespace socd {
// it's a tie; instead of none, we'll pick a direction // it's a tie; instead of none, we'll pick a direction
return SocdCW; return SocdCW;
} }
} else { } else if (algo == SocdAlgorithm::Neutral) {
// SOCD: neutral when both are pressed // SOCD: neutral when both are pressed
return SocdNone; return SocdNone;
} else if (algo == SocdAlgorithm::None) {
// SOCD: both are pressed
return SocdBoth;
} else {
log_fatal("socd", "invalid SOCD algorithm");
return SocdNone;
} }
} }
@@ -97,7 +108,8 @@ namespace socd {
log_fatal("socd", "invalid device index in socd_clean: {}", device); log_fatal("socd", "invalid device index in socd_clean: {}", device);
} }
const auto socd_result = socd_clean(device, up, down, time_now); const auto socd_result = socd_clean(
device + 2, up, down, time_now, SocdAlgorithm::PreferRecent);
if (up) { if (up) {
most_recent_active[device][TiltUp] = time_now; most_recent_active[device][TiltUp] = time_now;
@@ -125,7 +137,7 @@ namespace socd {
result = TiltUp; result = TiltUp;
} else if (socd_result == SocdCW) { } else if (socd_result == SocdCW) {
result = TiltDown; result = TiltDown;
} else { } else if (socd_result == SocdBoth) {
result = TiltUp; result = TiltUp;
} }
} else if (is_up) { } else if (is_up) {
+11 -3
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <optional>
#include <cstdint> #include <cstdint>
namespace socd { namespace socd {
@@ -9,7 +10,8 @@ namespace socd {
enum class SocdAlgorithm { enum class SocdAlgorithm {
Neutral, Neutral,
PreferRecent, PreferRecent,
PreferFirst PreferFirst,
None,
}; };
extern SocdAlgorithm ALGORITHM; extern SocdAlgorithm ALGORITHM;
@@ -17,10 +19,16 @@ namespace socd {
typedef enum _SocdResult { typedef enum _SocdResult {
SocdCCW = 0, SocdCCW = 0,
SocdCW = 1, SocdCW = 1,
SocdNone = 2 SocdNone = 2,
SocdBoth = 3
} SocdResult; } SocdResult;
SocdResult socd_clean(uint8_t device, bool ccw, bool cw, double time_now); SocdResult socd_clean(
uint8_t device,
bool ccw,
bool cw,
double time_now,
std::optional<SocdAlgorithm> algorithm = std::nullopt);
// for guitar wail (up/down only) // for guitar wail (up/down only)