diff --git a/include/dusk/gyro.h b/include/dusk/gyro.h index 279aeae16e..a206100739 100644 --- a/include/dusk/gyro.h +++ b/include/dusk/gyro.h @@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az); extern bool s_sensor_keep_alive; bool get_sensor_keep_alive(); void set_sensor_keep_alive(bool value); +bool rollgoal_gyro_enabled(); } // namespace dusk::gyro #endif diff --git a/include/dusk/settings.h b/include/dusk/settings.h index d9d85fa36b..f74a4bf61f 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -27,6 +27,11 @@ enum class DiscVerificationState : u8 { HashMismatch, }; +enum class GyroMode : u8 { + Sensor = 0, + Mouse = 1, +}; + namespace config { template <> struct ConfigEnumRange { @@ -45,6 +50,12 @@ struct ConfigEnumRange { static constexpr auto min = DiscVerificationState::Unknown; static constexpr auto max = DiscVerificationState::HashMismatch; }; + +template <> +struct ConfigEnumRange { + static constexpr auto min = GyroMode::Sensor; + static constexpr auto max = GyroMode::Mouse; +}; } // Persistent user settings @@ -120,6 +131,7 @@ struct UserSettings { ConfigVar midnasLamentNonStop; // Input + ConfigVar gyroMode; ConfigVar enableGyroAim; ConfigVar enableGyroRollgoal; ConfigVar gyroSensitivityX; diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index f0905efd5d..0d5b6dd2c9 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -729,10 +729,12 @@ static void koro2_game(fshop_class* i_this) { cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200); case 2: #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { if (!dusk::gyro::get_sensor_keep_alive()) { dusk::gyro::set_sensor_keep_alive(true); } + } else if (dusk::gyro::get_sensor_keep_alive()) { + dusk::gyro::set_sensor_keep_alive(false); } #endif @@ -753,7 +755,7 @@ static void koro2_game(fshop_class* i_this) { old_stick_x = mDoCPd_c::getSubStickX(PAD_1); cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000); #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTick(true, i_this->field_0x4060); } #endif @@ -791,7 +793,7 @@ static void koro2_game(fshop_class* i_this) { s16 gyro_ax = 0; s16 gyro_az = 0; #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); } #endif diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index f4af7a2961..940797c9ce 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -156,6 +156,7 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index abe22909c1..4011cbb721 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -1,5 +1,9 @@ #include "dusk/gyro.h" +#include "dusk/ui/ui.hpp" #include "d/actor/d_a_alink.h" + +#include +#include #include namespace dusk::gyro { @@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f; constexpr float kMinGravityProjection = 0.2f; // Let roll contribute more strongly as the pad approaches an upright posture. constexpr float kRollAimBoostMax = 2.0f; +constexpr float kMousePixelToRad = 0.0025f; bool s_sensor_enabled = false; bool s_accel_enabled = false; bool s_was_aiming = false; bool s_have_gravity_baseline = false; +bool s_mouse_enabled = false; +bool s_mouse_relative = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; @@ -36,6 +43,7 @@ void reset_filter_state() { s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; s_was_aiming = false; s_have_gravity_baseline = false; + s_mouse_enabled = false; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) { } return v; } + +void disable_pad_sensors() { + if (s_sensor_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); + s_sensor_enabled = false; + } + if (s_accel_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); + s_accel_enabled = false; + } +} } // namespace bool s_sensor_keep_alive = false; bool get_sensor_keep_alive() { return s_sensor_keep_alive; } void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; } +bool rollgoal_gyro_enabled() { + return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse; +} + bool queryGyroAimContext() { - if (!static_cast(dusk::getSettings().game.enableGyroAim)) { + if (!static_cast(getSettings().game.enableGyroAim)) { return false; } @@ -71,15 +94,28 @@ void read(float dt) { const bool aim_just_ended = !aim_active && s_was_aiming; s_was_aiming = aim_active; + const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse; + const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive); + SDL_Window* window = aurora::window::get_sdl_window(); + if (window != nullptr && mouse_gyro_active != s_mouse_relative && + SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active)) + { + s_mouse_relative = mouse_gyro_active; + } + + if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) { + const AuroraWindowSize sz = aurora::window::get_window_size(); + const float cx = static_cast(sz.width) * 0.5f; + const float cy = static_cast(sz.height) * 0.5f; + SDL_WarpMouseInWindow(window, cx, cy); + float discard_x = 0.0f; + float discard_y = 0.0f; + SDL_GetRelativeMouseState(&discard_x, &discard_y); + } + s_mouse_enabled = mouse_gyro_active; + if (!s_sensor_keep_alive && !aim_active) { - if (s_sensor_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); - s_sensor_enabled = false; - } - if (s_accel_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); - s_accel_enabled = false; - } + disable_pad_sensors(); reset_filter_state(); return; } @@ -90,6 +126,31 @@ void read(float dt) { s_have_gravity_baseline = false; } + if (mouse_mode && !mouse_gyro_active) { + s_pitch_rad = 0.0f; + s_yaw_rad = 0.0f; + s_roll_rad = 0.0f; + return; + } + + if (mouse_mode) { + disable_pad_sensors(); + + float mx_rel = 0.0f; + float my_rel = 0.0f; + SDL_GetRelativeMouseState(&mx_rel, &my_rel); + // Convert pixels to radians + s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; + s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY; + s_roll_rad = 0.0f; + + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + + return; + } + if (!s_sensor_enabled) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { return; @@ -112,8 +173,8 @@ void read(float dt) { return; } - const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); - const float deadband = dusk::getSettings().game.gyroDeadband; + const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); + const float deadband = getSettings().game.gyroDeadband; s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx); s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); @@ -123,8 +184,8 @@ void read(float dt) { const float yaw_rate = apply_deadband(s_smooth_gy, deadband); const float roll_rate = apply_deadband(s_smooth_gz, deadband); - s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; - s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityX; + s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X float horizontal_rate = yaw_rate; if (aim_active && s_accel_enabled) { @@ -162,11 +223,11 @@ void read(float dt) { } } - s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; + s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityY; - s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; - s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; - s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; } void getAimDeltas(float& out_yaw, float& out_pitch) { @@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) { return; } - float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; + float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal; + float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal; + roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; s_rollgoal_az += cM_rad2s(roll_rad); cXyz in(roll_rad, 0.0f, pitch_rad); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index c8e7178d67..f0d10d4226 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -68,6 +68,7 @@ UserSettings g_userSettings = { .midnasLamentNonStop {"game.midnasLamentNonStop", false}, // Input + .gyroMode {"game.gyroMode", GyroMode::Sensor}, .enableGyroAim {"game.enableGyroAim", false}, .enableGyroRollgoal {"game.enableGyroRollgoal", false}, .gyroSensitivityX {"game.gyroSensitivityX", 1.0f}, @@ -206,6 +207,7 @@ void registerSettings() { Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.enableFrameInterpolation); + Register(g_userSettings.game.gyroMode); Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroRollgoal); Register(g_userSettings.game.gyroSensitivityX); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index cbfde1ba3a..fd7365a39d 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -13,6 +13,7 @@ #include "m_Do/m_Do_main.h" #include "menu_bar.hpp" #include "number_button.hpp" +#include "menu_bar.hpp" #include "pane.hpp" #include "prelaunch.hpp" #include "ui.hpp" @@ -42,6 +43,11 @@ constexpr std::array kFpsOverlayCornerNames = { "Bottom Right", }; +constexpr std::array kGyroInputModeLabels = { + "Sensor", + "Mouse", +}; + bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) { if (backend == "auto") { outBackend = BACKEND_AUTO; @@ -200,7 +206,9 @@ int float_setting_percent(ConfigVar& var) { } bool gyro_enabled() { - return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal; + return getSettings().game.enableGyroAim || + (getSettings().game.enableGyroRollgoal && + getSettings().game.gyroMode.getValue() != GyroMode::Mouse); } struct ConfigBoolProps { @@ -634,12 +642,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return !getSettings().game.freeCamera; }); leftPane.add_section("Gyro"); + leftPane.register_control( + leftPane.add_select_button({ + .key = "Gyro Input Method", + .getValue = + [] { + const auto mode = getSettings().game.gyroMode.getValue(); + const auto idx = static_cast(mode); + return Rml::String{kGyroInputModeLabels[idx]}; + }, + .isModified = + [] { + return getSettings().game.gyroMode.getValue() != + getSettings().game.gyroMode.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + for (size_t i = 0; i < kGyroInputModeLabels.size(); i++) { + pane + .add_button({ + .text = Rml::String{kGyroInputModeLabels[i]}, + .isSelected = + [i] { + return getSettings().game.gyroMode.getValue() == static_cast(i); + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + const GyroMode mode = static_cast(i); + getSettings().game.gyroMode.setValue(mode); + config::Save(); + }); + } + pane.add_rml( + "
Sensor reads motion directly from a supported controller's gyro via SDL.
" + "
Mouse treats mouse input as gyro, intended for use with the Steam Deck.
" + "
Mouse input cannot currently be used with Gyro Rollgoal."); + }); addOption("Gyro Aim", getSettings().game.enableGyroAim, "Enables gyro controls while in look mode, aiming a hawk, and aiming " "supported items.

Supported items include the Slingshot, Gale Boomerang, " "Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod."); addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal, - "Enables gyro controls for Rollgoal in Hena's Cabin."); + "Enables gyro controls for Rollgoal in Hena's Cabin.", + [] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; }); config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY, "Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5, [] { return !gyro_enabled(); }); @@ -648,7 +694,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return !gyro_enabled(); }); config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal, "Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.", - 25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; }); + 25, 400, 5, + [] { + return !getSettings().game.enableGyroRollgoal || + getSettings().game.gyroMode.getValue() == GyroMode::Mouse; + }); config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband", "Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1, [] { return !gyro_enabled(); });