diff --git a/files.cmake b/files.cmake index ebfa843b24..ac7a2dd05d 100644 --- a/files.cmake +++ b/files.cmake @@ -1432,6 +1432,7 @@ set(DUSK_FILES src/dusk/game_clock.cpp src/dusk/globals.cpp src/dusk/gyro.cpp + src/dusk/mouse.cpp src/dusk/gamepad_color.cpp src/dusk/autosave.cpp src/dusk/http/http.hpp diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 9f69900bcb..4fb8abd929 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4551,7 +4551,7 @@ class daAlink_c : public daPy_py_c { #if TARGET_PC void handleWolfHowl(); void handleQuickTransform(); - bool checkGyroAimContext(); + bool checkAimContext(); void onIronBallChainInterpCallback(); diff --git a/include/d/d_camera.h b/include/d/d_camera.h index 7b12493360..0698bbbe5a 100644 --- a/include/d/d_camera.h +++ b/include/d/d_camera.h @@ -1037,6 +1037,7 @@ class dCamera_c { bool test1Camera(s32); bool test2Camera(s32); #if TARGET_PC + static bool canUseFreeCam(); bool freeCamera(); bool executeDebugFlyCam(); void deactivateDebugFlyCam(); diff --git a/include/dusk/gyro.h b/include/dusk/gyro.h index a206100739..abb56640e5 100644 --- a/include/dusk/gyro.h +++ b/include/dusk/gyro.h @@ -1,5 +1,4 @@ -#ifndef DUSK_GYRO_H -#define DUSK_GYRO_H +#pragma once namespace dusk::gyro { void read(float dt); @@ -14,5 +13,3 @@ 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/mouse.h b/include/dusk/mouse.h new file mode 100644 index 0000000000..514d14534a --- /dev/null +++ b/include/dusk/mouse.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace dusk::mouse { +void read(); +void getAimDeltas(float& out_yaw, float& out_pitch); +void getCameraDeltas(float& out_yaw, float& out_pitch); +void handle_event(const SDL_Event& event) noexcept; +void onFocusLost(); +void onFocusGained(); +} // namespace dusk::mouse diff --git a/include/dusk/settings.h b/include/dusk/settings.h index e36f0b9f24..74154a823a 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -187,7 +187,6 @@ struct UserSettings { ConfigVar midnasLamentNonStop; // Input - ConfigVar gyroMode; ConfigVar enableGyroAim; ConfigVar enableGyroRollgoal; ConfigVar gyroSensitivityX; @@ -197,6 +196,11 @@ struct UserSettings { ConfigVar gyroDeadband; ConfigVar gyroInvertPitch; ConfigVar gyroInvertYaw; + ConfigVar enableMouseCamera; + ConfigVar enableMouseAim; + ConfigVar mouseAimSensitivity; + ConfigVar mouseCameraSensitivity; + ConfigVar invertMouseY; ConfigVar freeCamera; ConfigVar invertCameraXAxis; ConfigVar invertCameraYAxis; diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index ef14eadd9f..ac0ea03bb9 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -144,7 +144,7 @@ void daAlink_c::handleQuickTransform() { procCoMetamorphoseInit(); } -bool daAlink_c::checkGyroAimContext() { +bool daAlink_c::checkAimContext() { switch (mProcID) { case PROC_SUBJECTIVITY: case PROC_SWIM_SUBJECTIVITY: diff --git a/src/d/actor/d_a_alink_link.inc b/src/d/actor/d_a_alink_link.inc index 636aabdffe..4d93f39457 100644 --- a/src/d/actor/d_a_alink_link.inc +++ b/src/d/actor/d_a_alink_link.inc @@ -11,8 +11,9 @@ #include "d/actor/d_a_tag_mhint.h" #if TARGET_PC -#include "dusk/gyro.h" #include "dusk/action_bindings.h" +#include "dusk/gyro.h" +#include "dusk/mouse.h" #endif bool daAlink_c::checkNoSubjectModeCamera() { @@ -120,18 +121,28 @@ BOOL daAlink_c::setBodyAngleToCamera() { var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c); } - shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f))); - sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f))); - - if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) { +#if TARGET_PC + if (dusk::getSettings().game.enableMouseAim && checkAimContext()) { sp8 = mBodyAngle.x; + } else +#endif + { + shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f))); + sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f))); + + if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) { + sp8 = mBodyAngle.x; + } } } else { sp8 = mBodyAngle.x; } #if TARGET_PC - if (dusk::getSettings().game.enableGyroAim && checkGyroAimContext()) { + if ((dusk::getSettings().game.enableGyroAim || + dusk::getSettings().game.enableMouseAim) && + checkAimContext()) + { f32 gyro_scale = 1.0f; if (checkWolfEyeUp()) { gyro_scale *= 0.6f; @@ -141,12 +152,21 @@ BOOL daAlink_c::setBodyAngleToCamera() { gyro_scale /= dComIfGp_getCameraZoomScale(field_0x317c); } - f32 gy_yaw = 0.f; - f32 gy_pitch = 0.f; - dusk::gyro::getAimDeltas(gy_yaw, gy_pitch); + f32 final_yaw = 0.f; + f32 final_pitch = 0.f; + if (dusk::getSettings().game.enableMouseAim) { + dusk::mouse::getAimDeltas(final_yaw, final_pitch); + } + if (dusk::getSettings().game.enableGyroAim) { + f32 gyro_yaw = 0.f; + f32 gyro_pitch = 0.f; + dusk::gyro::getAimDeltas(gyro_yaw, gyro_pitch); + final_yaw += gyro_yaw; + final_pitch += gyro_pitch; + } - shape_angle.y = shape_angle.y + cM_rad2s(gy_yaw * gyro_scale); - sp8 = sp8 + cM_rad2s(gy_pitch * gyro_scale); + shape_angle.y = shape_angle.y + cM_rad2s(final_yaw * gyro_scale); + sp8 = sp8 + cM_rad2s(final_pitch * gyro_scale); if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) { sp8 = mBodyAngle.x; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 8e7e48949a..50e9f073b6 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -32,6 +32,8 @@ #include "dusk/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/action_bindings.h" +#include "dusk/mouse.h" +#include "dusk/settings.h" #include "imgui.h" #endif @@ -7638,12 +7640,16 @@ void dCamera_c::deactivateDebugFlyCam() { mDebugFlyCam.initialized = false; } +bool dCamera_c::canUseFreeCam() { + return dusk::getSettings().game.freeCamera || dusk::getSettings().game.enableMouseCamera; +} + bool dCamera_c::freeCamera() { - if (dusk::getSettings().game.freeCamera && mGear == 1) { + if (canUseFreeCam() && mGear == 1) { mGear = 0; } - if (!dusk::getSettings().game.freeCamera || mCamStyle == 70) + if (!canUseFreeCam() || mCamStyle == 70) { mCamParam.mManualMode = 0; return false; @@ -7669,6 +7675,17 @@ bool dCamera_c::freeCamera() { mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraYSensitivity * 5.0f; } + f32 yaw_rad = 0.0f; + f32 pitch_rad = 0.0f; + dusk::mouse::getCameraDeltas(yaw_rad, pitch_rad); + if (dusk::getSettings().game.enableMouseCamera && (yaw_rad != 0.0f || pitch_rad != 0.0f) && + !dComIfGp_checkCameraAttentionStatus(dComIfGp_getPlayerCameraID(0), 0x8)) + { + mCamParam.mManualMode = 1; + mCamParam.freeXAngle += MTXRadToDeg(yaw_rad); + mCamParam.freeYAngle += -MTXRadToDeg(pitch_rad); + } + fopAc_ac_c* player = dComIfGp_getPlayer(0); if (!mCamParam.mManualMode || player == nullptr) { return false; @@ -9350,6 +9367,10 @@ bool dCamera_c::rideCamera(s32 param_0) { mStyleSettle.mFinished = true; } +#if TARGET_PC + freeCamera(); +#endif + return true; } @@ -9479,6 +9500,10 @@ bool dCamera_c::rideCamera(s32 param_0) { setFlag(0x400); } +#if TARGET_PC + freeCamera(); +#endif + return true; } diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index 680d500631..1e2e379960 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -2,8 +2,6 @@ #include "dusk/ui/ui.hpp" #include "d/actor/d_a_alink.h" -#include -#include #include namespace dusk::gyro { @@ -16,14 +14,11 @@ 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; @@ -43,7 +38,6 @@ 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; } @@ -72,7 +66,7 @@ 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; + return getSettings().game.enableGyroRollgoal; } bool queryGyroAimContext() { @@ -85,7 +79,7 @@ bool queryGyroAimContext() { return false; } - return link->checkGyroAimContext() && dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); + return link->checkAimContext() && dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); } void read(float dt) { @@ -94,26 +88,6 @@ 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) { disable_pad_sensors(); reset_filter_state(); @@ -126,31 +100,6 @@ 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.gyroSensitivityY; - s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; - 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; diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index d8b85b2d63..5248bc9d36 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -12,7 +12,6 @@ #include "ImGuiConsole.hpp" #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" -#include "SDL3/SDL_mouse.h" #include "dusk/action_bindings.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" @@ -61,10 +60,6 @@ namespace dusk { ImGui::TextUnformatted(text.data(), text.data() + text.size()); } - void DuskToast(std::string_view message, float duration) { - g_imguiConsole.AddToast(message, duration); - } - void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize( @@ -376,22 +371,6 @@ namespace dusk { m_menuTools.ShowActorSpawner(); } - // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. - if (dusk::getSettings().game.gyroMode.getValue() != GyroMode::Mouse) - { - ImGuiIO& io = ImGui::GetIO(); - if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { - mouseHideTimer = 0.0f; - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. - } else if (mouseHideTimer <= 3.0f) { - mouseHideTimer += ImGui::GetIO().DeltaTime; - } else { - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; - SDL_HideCursor(); - } - } - - ShowToasts(); } void ImGuiConsole::PostDraw() { @@ -545,50 +524,6 @@ namespace dusk { return false; } - void ImGuiConsole::AddToast(std::string_view message, float duration) { - m_toasts.emplace_back(std::string(message), duration); - } - - void ImGuiConsole::ShowToasts() { - if (m_toasts.empty()) { - return; - } - auto& toast = m_toasts.front(); - const float dt = ImGui::GetIO().DeltaTime; - toast.remain -= dt; - toast.current += dt; - - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - const ImVec2 workPos = viewport->WorkPos; - const ImVec2 workSize = viewport->WorkSize; - constexpr float padding = 10.0f; - const ImVec2 windowPos{workPos.x + workSize.x / 2, workPos.y + workSize.y - padding}; - ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, ImVec2{0.5f, 1.f}); - - const float alpha = std::min({toast.remain, toast.current, 1.f}); - ImGui::SetNextWindowBgAlpha(alpha * 0.65f); - ImVec4 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); - textColor.w *= alpha; - ImVec4 borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border); - borderColor.w *= alpha; - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - ImGui::PushStyleColor(ImGuiCol_Border, borderColor); - if (ImGui::Begin("Toast", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_NoMove)) - { - ImGuiStringViewText(toast.message); - } - ImGui::End(); - ImGui::PopStyleColor(2); - - if (toast.remain <= 0.f) { - m_toasts.pop_front(); - } - } - void ImGuiConsole::ShowPipelineProgress() { const auto* stats = aurora_get_stats(); const u32 queuedPipelines = stats->queuedPipelines; diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index c1adc427eb..d97f1b00e6 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -24,29 +24,17 @@ class ImGuiConsole { void PostDraw(); static bool CheckMenuViewToggle(ImGuiKey key, bool& active); - void AddToast(std::string_view message, float duration = 3.f); private: - struct Toast { - std::string message; - float remain; - float current = 0.f; - Toast(std::string message, float duration) noexcept : message(std::move(message)), - remain(duration) {} - }; - - float mouseHideTimer = 0.0f; bool m_isHidden = true; bool m_isLaunchInitialized = false; ImGuiWindow* m_dragScrollWindow = nullptr; ImVec2 m_dragScrollLastMousePos = {}; - std::deque m_toasts; // Keep always last ImGuiMenuTools m_menuTools; - void ShowToasts(); void ShowPipelineProgress(); void UpdateDragScroll(); }; @@ -60,7 +48,6 @@ std::string BytesToString(size_t bytes); void SetOverlayWindowLocation(int corner); bool ShowCornerContextMenu(int& corner, int avoidCorner); void ImGuiStringViewText(std::string_view text); -void DuskToast(std::string_view message, float duration = 3.f); void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); void ImGuiEndGroupPanel(); void ImGuiTextCenter(std::string_view text); diff --git a/src/dusk/mouse.cpp b/src/dusk/mouse.cpp new file mode 100644 index 0000000000..8ebfaf60cb --- /dev/null +++ b/src/dusk/mouse.cpp @@ -0,0 +1,211 @@ +#include "dusk/mouse.h" +#include "dusk/settings.h" +#include "dusk/ui/ui.hpp" +#include "d/actor/d_a_alink.h" +#include "d/d_com_inf_game.h" + +#include +#include +#include +#include + +namespace dusk::mouse { +namespace { +constexpr float kMousePixelToRad = 0.0025f; +constexpr int kIdleHideFrames = 99; // Approx. 3 seconds with 33ms ticks + +float s_aim_yaw_rad = 0.0f; +float s_aim_pitch_rad = 0.0f; +float s_camera_yaw_rad = 0.0f; +float s_camera_pitch_rad = 0.0f; +int s_idle_frames = 0; + +void reset_deltas() { + s_aim_yaw_rad = s_aim_pitch_rad = 0.0f; + s_camera_yaw_rad = s_camera_pitch_rad = 0.0f; +} + +bool queryMouseAimContext() { + if (!getSettings().game.enableMouseAim) { + return false; + } + + daAlink_c* link = daAlink_getAlinkActorClass(); + if (link == nullptr) { + return false; + } + + return link->checkAimContext() && dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); +} + +bool wantMouseCapture() { + return getSettings().game.enableMouseCamera.getValue() || queryMouseAimContext(); +} + +bool isWindowFocused(SDL_Window* window) { + if (window == nullptr) { + return false; + } + return (SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS) != 0; +} + +bool shouldCaptureMouse(SDL_Window* window) { + if (window == nullptr || ui::any_document_visible()) { + return false; + } + return wantMouseCapture() && isWindowFocused(window); +} + +bool syncCaptureState(SDL_Window* window, bool should_capture) { + if (window == nullptr) { + reset_deltas(); + return false; + } + + const bool was_captured = SDL_GetWindowRelativeMouseMode(window); + if (was_captured != should_capture) { + SDL_SetWindowMouseGrab(window, should_capture); + SDL_SetWindowRelativeMouseMode(window, should_capture); + } + + const bool is_captured = SDL_GetWindowRelativeMouseMode(window); + if (is_captured && !was_captured) { + 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); + } + + if (!is_captured) { + reset_deltas(); + } + + return is_captured; +} + +void accumulateDeltas(float mx_rel, float my_rel, bool camera_active, bool aim_active) { + const auto& game = getSettings().game; + const bool mirror_mode = game.enableMirrorMode.getValue(); + const bool invert_y = game.invertMouseY.getValue(); + + if (aim_active) { + const float aimSens = game.mouseAimSensitivity.getValue(); + s_aim_yaw_rad = -mx_rel * kMousePixelToRad * aimSens; + s_aim_pitch_rad = my_rel * kMousePixelToRad * aimSens; + s_aim_yaw_rad = mirror_mode ? -s_aim_yaw_rad : s_aim_yaw_rad; + s_aim_pitch_rad = invert_y ? -s_aim_pitch_rad : s_aim_pitch_rad; + } else { + s_aim_yaw_rad = s_aim_pitch_rad = 0.0f; + } + + if (camera_active) { + const float camSens = game.mouseCameraSensitivity.getValue(); + s_camera_yaw_rad = -mx_rel * kMousePixelToRad * camSens; + s_camera_pitch_rad = -my_rel * kMousePixelToRad * camSens; + s_camera_yaw_rad = mirror_mode ? -s_camera_yaw_rad : s_camera_yaw_rad; + s_camera_pitch_rad = invert_y ? -s_camera_pitch_rad : s_camera_pitch_rad; + } else { + s_camera_yaw_rad = s_camera_pitch_rad = 0.0f; + } +} + +void set_cursor_visible(bool visible) { + if (visible) { + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; + SDL_ShowCursor(); + } else { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + SDL_HideCursor(); + } +} + +void update_cursor_visibility(SDL_Window* window, bool captured) { + if (window == nullptr || !isWindowFocused(window)) { + return; + } + + if (captured) { + s_idle_frames = 0; + set_cursor_visible(false); + return; + } + + const ImGuiIO& io = ImGui::GetIO(); + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { + s_idle_frames = 0; + set_cursor_visible(true); + return; + } + + if (s_idle_frames < kIdleHideFrames) { + ++s_idle_frames; + set_cursor_visible(true); + } else { + set_cursor_visible(false); + } +} +} // namespace + +void read() { + SDL_Window* window = aurora::window::get_sdl_window(); + const bool capture_active = syncCaptureState(window, shouldCaptureMouse(window)); + update_cursor_visibility(window, capture_active); + + if (!capture_active) { + return; + } + + const bool aim_active = capture_active && queryMouseAimContext(); + const bool camera_active = capture_active && getSettings().game.enableMouseCamera; + + float mx_rel = 0.0f; + float my_rel = 0.0f; + SDL_GetRelativeMouseState(&mx_rel, &my_rel); + accumulateDeltas(mx_rel, my_rel, camera_active, aim_active); +} + +void getAimDeltas(float& out_yaw, float& out_pitch) { + out_yaw = s_aim_yaw_rad; + out_pitch = s_aim_pitch_rad; +} + +void getCameraDeltas(float& out_yaw, float& out_pitch) { + out_yaw = 0.0f; + out_pitch = 0.0f; + + if (!getSettings().game.enableMouseCamera) { + return; + } + + out_yaw = s_camera_yaw_rad; + out_pitch = s_camera_pitch_rad; +} + +void handle_event(const SDL_Event& event) noexcept { + switch (event.type) { + case SDL_EVENT_WINDOW_FOCUS_LOST: + onFocusLost(); + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + onFocusGained(); + break; + } +} + +void onFocusLost() { + SDL_Window* window = aurora::window::get_sdl_window(); + if (window != nullptr) { + syncCaptureState(window, false); + } + s_idle_frames = 0; + set_cursor_visible(true); +} + +void onFocusGained() { + SDL_Window* window = aurora::window::get_sdl_window(); + syncCaptureState(window, shouldCaptureMouse(window)); +} +} // namespace dusk::mouse diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index cc8d54a517..e24faaf86b 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -75,7 +75,6 @@ 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}, @@ -85,6 +84,11 @@ UserSettings g_userSettings = { .gyroDeadband {"game.gyroDeadband", 0.04f}, .gyroInvertPitch {"game.gyroInvertPitch", false}, .gyroInvertYaw {"game.gyroInvertYaw", false}, + .enableMouseCamera {"game.enableMouseCamera", false}, + .enableMouseAim {"game.enableMouseAim", false}, + .mouseAimSensitivity {"game.mouseAimSensitivity", 1.0f}, + .mouseCameraSensitivity {"game.mouseCameraSensitivity", 1.0f}, + .invertMouseY {"game.invertMouseY", false}, .freeCamera {"game.freeCamera", false}, .invertCameraXAxis {"game.invertCameraXAxis", false}, .invertCameraYAxis {"game.invertCameraYAxis", false}, @@ -283,7 +287,6 @@ void registerSettings() { Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.invincibleEnemies); 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); @@ -293,6 +296,11 @@ void registerSettings() { Register(g_userSettings.game.gyroSmoothing); Register(g_userSettings.game.gyroInvertPitch); Register(g_userSettings.game.gyroInvertYaw); + Register(g_userSettings.game.enableMouseCamera); + Register(g_userSettings.game.enableMouseAim); + Register(g_userSettings.game.mouseAimSensitivity); + Register(g_userSettings.game.mouseCameraSensitivity); + Register(g_userSettings.game.invertMouseY); Register(g_userSettings.game.freeCamera); Register(g_userSettings.game.debugFlyCam); Register(g_userSettings.game.debugFlyCamLockEvents); diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp index 4296ac27d9..a7bcc3f9ed 100644 --- a/src/dusk/ui/document.cpp +++ b/src/dusk/ui/document.cpp @@ -5,7 +5,6 @@ #include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" -#include namespace dusk::ui { namespace { @@ -107,7 +106,6 @@ bool Document::visible() const { bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { if (cmd == NavCommand::Menu) { - toggle_cursor_if_gyro(!visible()); mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen); toggle(); return true; @@ -115,17 +113,4 @@ bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { return false; } -void Document::toggle_cursor_if_gyro(bool cursor_enabled) { - if (dusk::getSettings().game.gyroMode.getValue() == GyroMode::Mouse) - { - if (cursor_enabled) { - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; - SDL_ShowCursor(); - } else { - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; - SDL_HideCursor(); - } - } -} - } // namespace dusk::ui diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp index eed489beb3..d0f4cae841 100644 --- a/src/dusk/ui/document.hpp +++ b/src/dusk/ui/document.hpp @@ -43,8 +43,6 @@ class Document { bool pending_close() const { return mPendingClose; } bool closed() const { return mClosed; } - void toggle_cursor_if_gyro(bool); - protected: virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd); diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 1be5c7abb0..068913d592 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -45,7 +45,6 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( mTabBar = std::make_unique(mRoot, TabBar::Props{ .onClose = [this] { - toggle_cursor_if_gyro(false); mDoAud_seStartMenu(kSoundMenuClose); hide(false); }, @@ -219,7 +218,6 @@ bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) { return true; } if (cmd == NavCommand::Cancel && visible()) { - toggle_cursor_if_gyro(false); mDoAud_seStartMenu(kSoundMenuClose); hide(false); return true; diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index ef6b6e5709..44b73b75ef 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -699,8 +699,6 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB return; } - toggle_cursor_if_gyro(false); - mDoAud_seStartMenu(kSoundPlay); show_menu_notification(); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 99dee7ace7..f01098a4c3 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -383,9 +383,7 @@ int float_setting_percent(ConfigVar& var) { } bool gyro_enabled() { - return getSettings().game.enableGyroAim || - (getSettings().game.enableGyroRollgoal && - getSettings().game.gyroMode.getValue() != GyroMode::Mouse); + return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal; } struct ConfigBoolProps { @@ -453,7 +451,7 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar
Applies to the control stick only.", + 50, 200, 5, [] { return !getSettings().game.freeCamera; }); config_percent_select(leftPane, rightPane, getSettings().game.freeCameraYSensitivity, - "Free Camera Y Sensitivity", "Adjusts twin-stick camera Y axis sensitivity.", 50, 200, 5, + "Free Camera Y Sensitivity", + "Adjusts vertical free camera sensitivity.

Applies to the control stick only.", + 50, 200, 5, [] { return !getSettings().game.freeCamera; }); + addOption("Invert Camera X Axis", getSettings().game.invertCameraXAxis, + "Invert horizontal camera movement.

Applies to the control stick only."); + addOption("Invert Camera Y Axis", getSettings().game.invertCameraYAxis, + "Invert vertical camera movement.

Applies to the control stick only.", [] { return !getSettings().game.freeCamera; }); addOption("Invert First Person X Axis", getSettings().game.invertFirstPersonXAxis, - "Invert horizontal movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings)."); + "Invert horizontal movement while aiming with items or first person camera.

Applies to the control stick only."); addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis, - "Invert vertical movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings)."); - addOption("Invert Air/Swim X Axis", getSettings().game.invertAirSwimX, - "Invert horizontal movement while flying or swimming."); - addOption("Invert Air/Swim Y Axis", getSettings().game.invertAirSwimY, - "Invert vertical movement while flying or swimming."); + "Invert vertical movement while aiming with items or first person camera.

Applies to the control stick only."); 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.", - [] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; }); + "Enables gyro controls for Rollgoal in Hena's Cabin."); config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY, "Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5, [] { return !gyro_enabled(); }); @@ -1025,10 +982,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { 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 || - getSettings().game.gyroMode.getValue() == GyroMode::Mouse; - }); + [] { return !getSettings().game.enableGyroRollgoal; }); 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(); }); @@ -1039,8 +993,29 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "Invert vertical gyro aiming.", [] { return !gyro_enabled(); }); addOption("Invert Gyro Yaw", getSettings().game.gyroInvertYaw, "Invert horizontal gyro aiming.", [] { return !gyro_enabled(); }); - + + leftPane.add_section("Mouse"); + addOption("Mouse Aim", getSettings().game.enableMouseAim, + "Enables mouse input 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("Mouse Camera", getSettings().game.enableMouseCamera, + "Enables mouse input for controlling the third-person camera."); + config_percent_select(leftPane, rightPane, getSettings().game.mouseAimSensitivity, + "Mouse Aim Sensitivity", "Controls mouse aim sensitivity.", 25, 400, 5, + [] { return !getSettings().game.enableMouseAim; }); + config_percent_select(leftPane, rightPane, getSettings().game.mouseCameraSensitivity, + "Mouse Camera Sensitivity", "Controls mouse camera sensitivity.", 25, 400, 5, + [] { return !getSettings().game.enableMouseCamera; }); + addOption("Invert Mouse Y", getSettings().game.invertMouseY, + "Invert vertical mouse control for both aiming and camera.", + [] { return !getSettings().game.enableMouseAim || !getSettings().game.enableMouseCamera; }); + leftPane.add_section("Gameplay"); + addOption("Invert Air/Swim X Axis", getSettings().game.invertAirSwimX, + "Invert horizontal movement while flying or swimming."); + addOption("Invert Air/Swim Y Axis", getSettings().game.invertAirSwimY, + "Invert vertical movement while flying or swimming."); addOption("Swap Direct Select Input", getSettings().game.swapDirectSelect, "Swap the controls for using Direct Select on the item wheel, making Direct Select the default and holding L to scroll the wheel."); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index fbd872efc4..5e09d15bbd 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -55,6 +55,7 @@ #include "dusk/frame_interpolation.h" #include "dusk/game_clock.h" #include "dusk/gyro.h" +#include "dusk/mouse.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/imgui/ImGuiEngine.hpp" #include "dusk/iso_validate.hpp" @@ -167,6 +168,7 @@ bool launchUILoop() { while (event != nullptr && event->type != AURORA_NONE) { switch (event->type) { case AURORA_SDL_EVENT: + dusk::mouse::handle_event(event->sdl); dusk::ui::handle_event(event->sdl); dusk::g_imguiConsole.HandleSDLEvent(event->sdl); break; @@ -245,12 +247,15 @@ void main01(void) { goto eventsDone; case AURORA_PAUSED: dusk::audio::SetPaused(true); + dusk::mouse::onFocusLost(); break; case AURORA_UNPAUSED: dusk::audio::SetPaused(false); dusk::game_clock::reset_frame_timer(); + dusk::mouse::onFocusGained(); break; case AURORA_SDL_EVENT: + dusk::mouse::handle_event(event->sdl); dusk::ui::handle_event(event->sdl); dusk::g_imguiConsole.HandleSDLEvent(event->sdl); break; @@ -287,6 +292,7 @@ void main01(void) { for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) { dusk::frame_interp::begin_sim_tick(); mDoCPd_c::read(); + dusk::mouse::read(); dusk::gyro::read(pacing.sim_pace); fapGm_Execute(); mDoAud_Execute(); @@ -309,6 +315,7 @@ void main01(void) { // Game Inputs mDoCPd_c::read(); + dusk::mouse::read(); dusk::gyro::read(pacing.presentation_dt_seconds); // EXECUTE GAME LOGIC & RENDER