diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 787b058..62941f6 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -624,6 +624,7 @@ set(SOURCE_FILES ${SOURCE_FILES} util/execexe.cpp util/dependencies.cpp util/deferlog.cpp + util/socd_cleaner.cpp ) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES}) diff --git a/src/spice2x/acio/bi2a/bi2a.cpp b/src/spice2x/acio/bi2a/bi2a.cpp index 68b2235..9722333 100644 --- a/src/spice2x/acio/bi2a/bi2a.cpp +++ b/src/spice2x/acio/bi2a/bi2a.cpp @@ -9,11 +9,22 @@ #include "games/drs/drs.h" #include "misc/eamuse.h" #include "util/logging.h" +#include "util/socd_cleaner.h" +#include "util/time.h" #include "util/utils.h" #include "util/tapeled.h" using namespace GameAPI; +#define DEBUG_VERBOSE 0 + +#if DEBUG_VERBOSE +#define log_debug(module, format_str, ...) logger::push( \ + LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY) +#else +#define log_debug(module, format_str, ...) +#endif + // state static uint8_t STATUS_BUFFER[272] {}; static bool STATUS_BUFFER_FREEZE = false; @@ -94,18 +105,25 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() { } // volume left - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) { + const auto now = get_performance_milliseconds(); + const auto vol_l_state = socd::socd_clean(0, + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)), + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)), + now); + if (vol_l_state == socd::SocdCCW) { BI2A_VOLL = (BI2A_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023; - } - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) { + } else if (vol_l_state == socd::SocdCW) { BI2A_VOLL = (BI2A_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023; } // volume right - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) { + const auto vol_r_state = socd::socd_clean(1, + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)), + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)), + now); + if (vol_r_state == socd::SocdCCW) { BI2A_VOLR = (BI2A_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023; - } - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) { + } else if (vol_r_state == socd::SocdCW) { BI2A_VOLR = (BI2A_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023; } @@ -127,6 +145,12 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() { // save volumes in buffer *((uint16_t*) &STATUS_BUFFER[17]) = (uint16_t) ((vol_left) << 2); *((uint16_t*) &STATUS_BUFFER[19]) = (uint16_t) ((vol_right) << 2); + + log_debug( + "bi2a", + "knobs = {} {}", + *((uint16_t*) &STATUS_BUFFER[17]), + *((uint16_t*) &STATUS_BUFFER[19])); } // DanceDanceRevolution diff --git a/src/spice2x/acio/kfca/kfca.cpp b/src/spice2x/acio/kfca/kfca.cpp index 079208c..3ead38e 100644 --- a/src/spice2x/acio/kfca/kfca.cpp +++ b/src/spice2x/acio/kfca/kfca.cpp @@ -8,10 +8,21 @@ #include "games/sdvx/io.h" #include "misc/eamuse.h" #include "rawinput/rawinput.h" +#include "util/socd_cleaner.h" +#include "util/time.h" #include "util/utils.h" using namespace GameAPI; +#define DEBUG_VERBOSE 0 + +#if DEBUG_VERBOSE +#define log_debug(module, format_str, ...) logger::push( \ + LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY) +#else +#define log_debug(module, format_str, ...) +#endif + // globals uint8_t KFCA_VOL_SOUND = 96; uint8_t KFCA_VOL_HEADPHONE = 96; @@ -342,18 +353,25 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() { } // volume left - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) { + const auto now = get_performance_milliseconds(); + const auto vol_l_state = socd::socd_clean(0, + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)), + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)), + now); + if (vol_l_state == socd::SocdCCW) { KFCA_VOLL = (KFCA_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023; - } - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) { + } else if (vol_l_state == socd::SocdCW) { KFCA_VOLL = (KFCA_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023; } // volume right - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) { + const auto vol_r_state = socd::socd_clean(1, + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)), + Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)), + now); + if (vol_r_state == socd::SocdCCW) { KFCA_VOLR = (KFCA_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023; - } - if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) { + } else if (vol_r_state == socd::SocdCW) { KFCA_VOLR = (KFCA_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023; } @@ -371,6 +389,8 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() { // proper loops vol_left %= 1024; vol_right %= 1024; + + log_debug("kfca", "knobs = {} {}", vol_left, vol_right); // save volumes in buffer STATUS_BUFFER[input_offset + 16 + 0] |= (unsigned char) ((vol_left << 6) & 0xFF); diff --git a/src/spice2x/games/iidx/iidx.cpp b/src/spice2x/games/iidx/iidx.cpp index 967c6dc..b1f05d4 100644 --- a/src/spice2x/games/iidx/iidx.cpp +++ b/src/spice2x/games/iidx/iidx.cpp @@ -29,6 +29,8 @@ #include "util/libutils.h" #include "util/memutils.h" #include "util/sigscan.h" +#include "util/socd_cleaner.h" +#include "util/time.h" #include "util/utils.h" #include "launcher/signal.h" @@ -507,6 +509,16 @@ namespace games::iidx { " monitor in Windows settings before launching the game" }); } + + // socd + socd::ALGORITHM = socd::SocdAlgorithm::Neutral; + if (options->at(launcher::Options::IIDXDigitalTTSocd).is_active()) { + if (options->at(launcher::Options::IIDXDigitalTTSocd).value_text() == "last") { + socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; + } else if (options->at(launcher::Options::IIDXDigitalTTSocd).value_text() == "first") { + socd::ALGORITHM = socd::SocdAlgorithm::PreferFirst; + } + } } void IIDXGame::detach() { @@ -730,13 +742,12 @@ namespace games::iidx { bool ttpm_alt = GameAPI::Buttons::getState(RI_MGR, buttons.at( player != 0 ? Buttons::P2_TTPlusMinusAlt : Buttons::P1_TTPlusMinusAlt)); - // TT+ - if (ttp) + const auto tt_socd = socd::socd_clean(player, ttm, ttp, get_performance_milliseconds()); + if (tt_socd == socd::SocdCW) { IIDXIO_TT_STATE[player] += change; - - // TT- - if (ttm) + } else if (tt_socd == socd::SocdCCW) { IIDXIO_TT_STATE[player] -= change; + } // TT+/- bool ttpm_rising_edge = !IIDXIO_TT_PRESSED[player] && ttpm; diff --git a/src/spice2x/games/sdvx/bi2x_hook.cpp b/src/spice2x/games/sdvx/bi2x_hook.cpp index 38f8a1c..5da92c2 100644 --- a/src/spice2x/games/sdvx/bi2x_hook.cpp +++ b/src/spice2x/games/sdvx/bi2x_hook.cpp @@ -12,9 +12,20 @@ #include "launcher/options.h" #include "io.h" #include "games/sdvx/sdvx.h" +#include "util/socd_cleaner.h" #include "util/tapeled.h" +#include "util/time.h" #include "acioemu/icca.h" +#define DEBUG_VERBOSE 0 + +#if DEBUG_VERBOSE +#define log_debug(module, format_str, ...) logger::push( \ + LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY) +#else +#define log_debug(module, format_str, ...) +#endif + namespace games::sdvx { constexpr bool BI2X_PASSTHROUGH = false; @@ -176,19 +187,27 @@ namespace games::sdvx { if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Headphone])) status->buffer[22] = 0x01; + const auto now = get_performance_milliseconds(); + // volume left - if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Left])) { + const auto vol_l_state = socd::socd_clean(0, + GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Left]), + GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Right]), + now); + if (vol_l_state == socd::SocdCCW) { VOL_L -= ((uint16_t)DIGITAL_KNOB_SENS * 4); - } - if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Right])) { + } else if (vol_l_state == socd::SocdCW) { VOL_L += ((uint16_t)DIGITAL_KNOB_SENS * 4); } // volume right - if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Left])) { + const auto vol_r_state = socd::socd_clean(1, + GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Left]), + GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Right]), + now); + if (vol_r_state == socd::SocdCCW) { VOL_R -= ((uint16_t)DIGITAL_KNOB_SENS * 4); - } - if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Right])) { + } else if (vol_r_state == socd::SocdCW) { VOL_R += ((uint16_t)DIGITAL_KNOB_SENS * 4); } @@ -203,6 +222,12 @@ namespace games::sdvx { *((uint16_t*) &status->buffer[312]) = vol_left; *((uint16_t*) &status->buffer[314]) = vol_right; + + log_debug( + "bi2x_hook", + "knobs = {} {}", + *((uint16_t*) &status->buffer[312]), + *((uint16_t*) &status->buffer[314])); } static void __fastcall AIO_IOB2_BI2X_UFC__IoReset(AIO_IOB2_BI2X_UFC *This, diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index 98525d0..fe56996 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -18,6 +18,8 @@ #include "util/detour.h" #include "util/logging.h" #include "util/sigscan.h" +#include "util/socd_cleaner.h" +#include "util/time.h" #include "util/libutils.h" #include "misc/wintouchemu.h" #include "misc/eamuse.h" @@ -360,6 +362,16 @@ namespace games::sdvx { this->detect_sound_output_device(); #endif + // SOCD + auto options = games::get_options(eamuse_get_game()); + socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; + if (options->at(launcher::Options::SDVXDigitalKnobSocd).is_active()) { + if (options->at(launcher::Options::SDVXDigitalKnobSocd).value_text() == "neutral") { + socd::ALGORITHM = socd::SocdAlgorithm::Neutral; + } else if (options->at(launcher::Options::SDVXDigitalKnobSocd).value_text() == "last") { + socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; + } + } } void SDVXGame::attach() { diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 295f570..563cd3e 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -604,7 +604,7 @@ static const std::vector OPTION_DEFINITIONS = { { .title = "IIDX TDJ Mode (Lightning Model)", .name = "iidxtdj", - .desc = "Enables TDJ cabinet mode. Ensure you also set -iidxsounddevice to desired option", + .desc = "Enables TDJ mode (Lightning Model cabinet).", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Game Options", @@ -621,6 +621,23 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Beatmania IIDX", .category = "Game Options (Advanced)", }, + { + // IIDXDigitalTTSocd + .title = "IIDX Digital TT SOCD Cleaner", + .name = "iidxsocd", + .desc = "SOCD for turntables when using button input; what happens when both directions are pressed.\n\n" + "last: most recently pressed direction takes priority\n\n" + "first: first pressed direction takes priority\n\n" + "neutral (default): TT does not move when both directions are pressed, recommended to avoid excessive POORs", + .type = OptionType::Enum, + .game_name = "Beatmania IIDX", + .category = "Game Options (Advanced)", + .elements = { + {"last", ""}, + {"first", ""}, + {"neutral", ""}, + }, + }, { // spice2x_IIDXLDJForce720p .title = "IIDX LDJ Force 720p (HD)", @@ -804,6 +821,23 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Game Options (Advanced)", }, + { + // SDVXDigitalKnobSocd + .title = "SDVX Digital Knob SOCD Cleaner", + .name = "sdvxsocd", + .desc = "SOCD for knobs when using button input; what happens when both directions are pressed.\n\n" + "last (default): most recently pressed direction takes priority, recommended to deal with slams\n\n" + "first: first pressed direction takes priority\n\n" + "neutral: knob does not move when both directions are pressed", + .type = OptionType::Enum, + .game_name = "Sound Voltex", + .category = "Game Options (Advanced)", + .elements = { + {"last", ""}, + {"first", ""}, + {"neutral", ""}, + }, + }, { // spice2x_SDVXAsioDriver .title = "SDVX ASIO driver", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 00a66f9..530d09c 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -67,6 +67,7 @@ namespace launcher { IIDXBIO2FW, IIDXTDJMode, spice2x_IIDXDigitalTTSensitivity, + IIDXDigitalTTSocd, spice2x_IIDXLDJForce720p, spice2x_IIDXTDJSubSize, spice2x_IIDXLEDFontSize, @@ -84,6 +85,7 @@ namespace launcher { SDVXDisableCameras, SDVXNativeTouch, spice2x_SDVXDigitalKnobSensitivity, + SDVXDigitalKnobSocd, spice2x_SDVXAsioDriver, spice2x_SDVXSubPos, spice2x_SDVXSubRedraw, diff --git a/src/spice2x/util/socd_cleaner.cpp b/src/spice2x/util/socd_cleaner.cpp new file mode 100644 index 0000000..0f7eb45 --- /dev/null +++ b/src/spice2x/util/socd_cleaner.cpp @@ -0,0 +1,87 @@ +#include "socd_cleaner.h" + +#include "util/logging.h" + +#define DEBUG_VERBOSE 0 + +#if DEBUG_VERBOSE +#define log_debug(module, format_str, ...) logger::push( \ + LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY) +#else +#define log_debug(module, format_str, ...) +#endif + +namespace socd { + + SocdAlgorithm ALGORITHM = SocdAlgorithm::Neutral; + + static double last_rising_edge[2][2] = {}; + static bool last_button_state[2][2] = {}; + + // 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) { + if (device >= 2) { + log_fatal("socd", "invalid device index in socd_clean: {}", device); + } + + // SOCD last input algorithm needs to keep track of rising edge times + if (ALGORITHM != SocdAlgorithm::Neutral) { + // detect rising edges + if (!last_button_state[device][SocdCCW] && ccw) { + last_rising_edge[device][SocdCCW] = time_now; + } + if (!last_button_state[device][SocdCW] && cw) { + last_rising_edge[device][SocdCW] = time_now; + } + + // update button state for next time + last_button_state[device][SocdCCW] = ccw; + last_button_state[device][SocdCW] = cw; + } + + // determine direction: easy cases + if (!ccw && !cw) { + return SocdNone; + } + if (ccw && !cw) { + return SocdCCW; + } + if (!ccw && cw) { + return SocdCW; + } + + // SOCD logic; depends on algorithm in use + const auto ccw_time = last_rising_edge[device][SocdCCW]; + const auto cw_time = last_rising_edge[device][SocdCW]; + log_debug("socd", "ccw={}, cw ={}", ccw_time, cw_time); + + if (ALGORITHM == SocdAlgorithm::PreferRecent) { + // SOCD: prefer last input + if (ccw_time < cw_time) { + // while CCW is being held, CW got pressed + return SocdCW; + } else if (ccw_time > cw_time) { + // while CW is being held, CCW got pressed + return SocdCCW; + } else { + // it's a tie; instead of none, we'll pick a direction + return SocdCW; + } + } else if (ALGORITHM == SocdAlgorithm::PreferFirst) { + // SOCD: keep first input + if (ccw_time < cw_time) { + // while CCW is being held, CW got pressed + return SocdCCW; + } else if (ccw_time > cw_time) { + // while CW is being held, CCW got pressed + return SocdCW; + } else { + // it's a tie; instead of none, we'll pick a direction + return SocdCW; + } + } else { + // SOCD: neutral when both are pressed + return SocdNone; + } + } +} diff --git a/src/spice2x/util/socd_cleaner.h b/src/spice2x/util/socd_cleaner.h new file mode 100644 index 0000000..0f662ad --- /dev/null +++ b/src/spice2x/util/socd_cleaner.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace socd { + + enum class SocdAlgorithm { + Neutral, + PreferRecent, + PreferFirst + }; + + extern SocdAlgorithm ALGORITHM; + + typedef enum _SocdResult { + SocdCCW = 0, + SocdCW = 1, + SocdNone = 2 + } SocdResult; + + SocdResult socd_clean(uint8_t device, bool ccw, bool cw, double time_now); +} \ No newline at end of file