diff --git a/assets/config_menu/general.rml b/assets/config_menu/general.rml index e2aacfa..31c625a 100644 --- a/assets/config_menu/general.rml +++ b/assets/config_menu/general.rml @@ -74,31 +74,50 @@ + +
+ +
+ + +
+
+ -
+
@@ -119,6 +138,12 @@ Note: To recalibrate controller gyro, set the controller down on a still, flat surface for 5 seconds.

+ Controls the sensitivity of mouse aiming when using items in first person for controllers that support it. Setting this to zero will disable mouse aiming. +
+
+ Note: This option does not allow mouse buttons to activate items. Mouse aiming is meant for using inputs that are mapped to mouse movement, such as gyro on Steam Deck. +

+

Allows the game to read controller input when out of focus.
This setting does not affect keyboard input. diff --git a/include/recomp_input.h b/include/recomp_input.h index 6315f48..dff7ba6 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -66,6 +66,7 @@ namespace recomp { bool get_input_digital(const InputField& field); bool get_input_digital(const std::span fields); void get_gyro_deltas(float* x, float* y); + void get_mouse_deltas(float* x, float* y); enum class InputDevice { Controller, @@ -125,9 +126,11 @@ namespace recomp { int get_rumble_strength(); void set_rumble_strength(int strength); - // Gyro sensitivity ranges from 0 to 100 (gets doubled). + // Gyro and mouse sensitivities range from 0 to 100. int get_gyro_sensitivity(); + int get_mouse_sensitivity(); void set_gyro_sensitivity(int strength); + void set_mouse_sensitivity(int strength); enum class TargetingMode { Switch, diff --git a/patches/input.c b/patches/input.c index 2710340..e76621e 100644 --- a/patches/input.c +++ b/patches/input.c @@ -9,7 +9,7 @@ s32 func_80847190(PlayState* play, Player* this, s32 arg2); s16 func_80832754(Player* this, s32 arg1); s32 func_8082EF20(Player* this); -// Patched to add gyro aiming +// @recomp Patched to add gyro and mouse aiming. s32 func_80847190(PlayState* play, Player* this, s32 arg2) { s32 pad; s16 var_s0; @@ -24,18 +24,19 @@ s32 func_80847190(PlayState* play, Player* this, s32 arg2) { } else { static float total_gyro_x, total_gyro_y; + static float total_mouse_x, total_mouse_y; static float filtered_gyro_x, filtered_gyro_y; - static int applied_gyro_x, applied_gyro_y; + static int applied_aim_x, applied_aim_y; - const float filter_factor = 0.00f; + const float gyro_filter_factor = 0.00f; - // TODO remappable gyro reset button - if (play->state.input[0].press.button & BTN_L) { - total_gyro_x = 0; - total_gyro_y = 0; - filtered_gyro_x = 0; - filtered_gyro_y = 0; - } + // // TODO remappable gyro reset button + // if (play->state.input[0].press.button & BTN_L) { + // total_gyro_x = 0; + // total_gyro_y = 0; + // filtered_gyro_x = 0; + // filtered_gyro_y = 0; + // } float delta_gyro_x, delta_gyro_y; recomp_get_gyro_deltas(&delta_gyro_x, &delta_gyro_y); @@ -43,18 +44,28 @@ s32 func_80847190(PlayState* play, Player* this, s32 arg2) { total_gyro_x += delta_gyro_x; total_gyro_y += delta_gyro_y; - filtered_gyro_x = filtered_gyro_x * filter_factor + total_gyro_x * (1.0f - filter_factor); - filtered_gyro_y = filtered_gyro_y * filter_factor + total_gyro_y * (1.0f - filter_factor); + filtered_gyro_x = filtered_gyro_x * gyro_filter_factor + total_gyro_x * (1.0f - gyro_filter_factor); + filtered_gyro_y = filtered_gyro_y * gyro_filter_factor + total_gyro_y * (1.0f - gyro_filter_factor); - int target_gyro_x = (int)filtered_gyro_x; - int target_gyro_y = (int)filtered_gyro_y; + float delta_mouse_x, delta_mouse_y; + recomp_get_mouse_deltas(&delta_mouse_x, &delta_mouse_y); + + total_mouse_x += delta_mouse_x; + total_mouse_y += delta_mouse_y; + + // The gyro X-axis (tilt) corresponds to the camera X-axis (tilt). + // The gyro Y-axis (left/right rotation) corresponds to the camera Y-axis (left/right rotation). + // The mouse Y-axis (up/down movement) corresponds to the camera X-axis (tilt). + // The mouse X-axis (left/right movement) corresponds to the camera Y-axis (left/right rotation). + int target_aim_x = (int)(filtered_gyro_x * -3.0f + total_mouse_y * 20.0f); + int target_aim_y = (int)(filtered_gyro_y * 3.0f + total_mouse_x * -20.0f); s16 temp3; temp3 = ((play->state.input[0].rel.stick_y >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(play->state.input[0].rel.stick_y * 0xC8)) * 1500.0f); - this->actor.focus.rot.x += temp3 + (s32)((target_gyro_x - applied_gyro_x) * -1.5f); - applied_gyro_x = target_gyro_x; + this->actor.focus.rot.x += temp3 + (s32)(target_aim_x - applied_aim_x); + applied_aim_x = target_aim_x; if (this->stateFlags1 & PLAYER_STATE1_800000) { this->actor.focus.rot.x = CLAMP(this->actor.focus.rot.x, -0x1F40, 0xFA0); @@ -66,8 +77,8 @@ s32 func_80847190(PlayState* play, Player* this, s32 arg2) { var_s0 = this->actor.focus.rot.y - this->actor.shape.rot.y; temp3 = ((play->state.input[0].rel.stick_x >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(play->state.input[0].rel.stick_x * 0xC8)) * -1500.0f); - var_s0 += temp3 + (s32)((target_gyro_y - applied_gyro_y) * 1.5f); - applied_gyro_y = target_gyro_y; + var_s0 += temp3 + (s32)(target_aim_y - applied_aim_y); + applied_aim_y = target_aim_y; this->actor.focus.rot.y = CLAMP(var_s0, -0x4AAA, 0x4AAA) + this->actor.shape.rot.y; } diff --git a/patches/input.h b/patches/input.h index b0d5154..d1399b7 100644 --- a/patches/input.h +++ b/patches/input.h @@ -11,6 +11,7 @@ typedef enum { extern RecompCameraMode recomp_camera_mode; DECLARE_FUNC(void, recomp_get_gyro_deltas, float* x, float* y); +DECLARE_FUNC(void, recomp_get_mouse_deltas, float* x, float* y); DECLARE_FUNC(int, recomp_get_targeting_mode); #endif diff --git a/patches/syms.ld b/patches/syms.ld index 5640bd2..4332214 100644 --- a/patches/syms.ld +++ b/patches/syms.ld @@ -46,3 +46,4 @@ osContStartReadData_recomp = 0x8F000070; osContGetReadData_recomp = 0x8F000074; osContStartQuery_recomp = 0x8F000078; osContGetQuery_recomp = 0x8F00007C; +recomp_get_mouse_deltas = 0x8F000080; diff --git a/src/game/config.cpp b/src/game/config.cpp index bbbd2de..698a3c2 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -130,6 +130,7 @@ void save_general_config(const std::filesystem::path& path) { recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode()); config_json["rumble_strength"] = recomp::get_rumble_strength(); config_json["gyro_sensitivity"] = recomp::get_gyro_sensitivity(); + config_json["mouse_sensitivity"] = recomp::get_mouse_sensitivity(); config_json["debug_mode"] = recomp::get_debug_mode_enabled(); config_file << std::setw(4) << config_json; } @@ -144,6 +145,7 @@ void load_general_config(const std::filesystem::path& path) { recomp::set_background_input_mode(from_or_default(config_json, "background_input_mode", recomp::BackgroundInputMode::On)); recomp::set_rumble_strength(from_or_default(config_json, "rumble_strength", 25)); recomp::set_gyro_sensitivity(from_or_default(config_json, "gyro_sensitivity", 50)); + recomp::set_mouse_sensitivity(from_or_default(config_json, "mouse_sensitivity", 0)); recomp::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false)); } diff --git a/src/game/input.cpp b/src/game/input.cpp index 73433d2..f1fe816 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -30,9 +30,13 @@ static struct { std::mutex cur_controllers_mutex; std::vector cur_controllers{}; std::unordered_map controller_states; + std::array rotation_delta{}; - std::mutex pending_rotation_mutex; + std::array mouse_delta{}; + std::mutex pending_input_mutex; std::array pending_rotation_delta{}; + std::array pending_mouse_delta{}; + float cur_rumble; bool rumble_active; } InputState; @@ -189,12 +193,19 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) { state.motion.GetPlayerSpaceGyro(rot_x, rot_y); { - std::lock_guard lock{ InputState.pending_rotation_mutex }; + std::lock_guard lock{ InputState.pending_input_mutex }; InputState.pending_rotation_delta[0] += rot_x; InputState.pending_rotation_delta[1] += rot_y; } } break; + case SDL_EventType::SDL_MOUSEMOTION: + if (!recomp::game_input_disabled()) { + SDL_MouseMotionEvent* motion_event = &event->motion; + std::lock_guard lock{ InputState.pending_input_mutex }; + InputState.pending_mouse_delta[0] += motion_event->xrel; + InputState.pending_mouse_delta[1] += motion_event->yrel; + } default: queue_if_enabled(event); break; @@ -207,7 +218,18 @@ void recomp::handle_events() { static bool exited = false; while (SDL_PollEvent(&cur_event) && !exited) { exited = sdl_event_filter(nullptr, &cur_event); - SDL_ShowCursor(cursor_enabled ? SDL_ENABLE : SDL_DISABLE); + + // Lock the cursor if all three conditions are true: mouse aiming is enabled, game input is not disabled, and the game has been started. + bool cursor_locked = (recomp::get_mouse_sensitivity() != 0) && !recomp::game_input_disabled() && ultramodern::is_game_started(); + + // Hide the cursor based on its enable state, but override visibility to false if the cursor is locked. + bool cursor_visible = cursor_enabled; + if (cursor_locked) { + cursor_visible = false; + } + + SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE); + SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE); } } @@ -352,9 +374,13 @@ void recomp::poll_inputs() { // Read the deltas while resetting them to zero. { - std::lock_guard lock{ InputState.pending_rotation_mutex }; + std::lock_guard lock{ InputState.pending_input_mutex }; + InputState.rotation_delta = InputState.pending_rotation_delta; InputState.pending_rotation_delta = { 0.0f, 0.0f }; + + InputState.mouse_delta = InputState.pending_mouse_delta; + InputState.pending_mouse_delta = { 0.0f, 0.0f }; } // Quicksaving is disabled for now and will likely have more limited functionality @@ -503,11 +529,18 @@ bool recomp::get_input_digital(const std::span fields) void recomp::get_gyro_deltas(float* x, float* y) { std::array cur_rotation_delta = InputState.rotation_delta; - float sensitivity = (float)recomp::get_gyro_sensitivity() / 50.0f; + float sensitivity = (float)recomp::get_gyro_sensitivity() / 100.0f; *x = cur_rotation_delta[0] * sensitivity; *y = cur_rotation_delta[1] * sensitivity; } +void recomp::get_mouse_deltas(float* x, float* y) { + std::array cur_mouse_delta = InputState.mouse_delta; + float sensitivity = (float)recomp::get_mouse_sensitivity() / 100.0f; + *x = cur_mouse_delta[0] * sensitivity; + *y = cur_mouse_delta[1] * sensitivity; +} + bool recomp::game_input_disabled() { // Disable input if any menu is open. return recomp::get_current_menu() != recomp::Menu::None; diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp index 7d7b8bd..8a82278 100644 --- a/src/game/recomp_api.cpp +++ b/src/game/recomp_api.cpp @@ -35,6 +35,13 @@ extern "C" void recomp_get_gyro_deltas(uint8_t* rdram, recomp_context* ctx) { recomp::get_gyro_deltas(x_out, y_out); } +extern "C" void recomp_get_mouse_deltas(uint8_t* rdram, recomp_context* ctx) { + float* x_out = _arg<0, float*>(rdram, ctx); + float* y_out = _arg<1, float*>(rdram, ctx); + + recomp::get_mouse_deltas(x_out, y_out); +} + extern "C" void recomp_powf(uint8_t* rdram, recomp_context* ctx) { float a = _arg<0, float>(rdram, ctx); float b = ctx->f14.fl; //_arg<1, float>(rdram, ctx); diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 94e909b..a74aacc 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -264,7 +264,8 @@ void open_quit_game_prompt() { struct ControlOptionsContext { int rumble_strength = 50; // 0 to 100 - int gyro_sensitivity = 50; // 0 to 200 + int gyro_sensitivity = 50; // 0 to 100 + int mouse_sensitivity = 50; // 0 to 100 recomp::TargetingMode targeting_mode = recomp::TargetingMode::Switch; recomp::BackgroundInputMode background_input_mode = recomp::BackgroundInputMode::On; }; @@ -286,6 +287,10 @@ int recomp::get_gyro_sensitivity() { return control_options_context.gyro_sensitivity; } +int recomp::get_mouse_sensitivity() { + return control_options_context.mouse_sensitivity; +} + void recomp::set_gyro_sensitivity(int sensitivity) { control_options_context.gyro_sensitivity = sensitivity; if (general_model_handle) { @@ -293,6 +298,13 @@ void recomp::set_gyro_sensitivity(int sensitivity) { } } +void recomp::set_mouse_sensitivity(int sensitivity) { + control_options_context.mouse_sensitivity = sensitivity; + if (general_model_handle) { + general_model_handle.DirtyVariable("mouse_sensitivity"); + } +} + recomp::TargetingMode recomp::get_targeting_mode() { return control_options_context.targeting_mode; } @@ -787,6 +799,7 @@ public: constructor.Bind("rumble_strength", &control_options_context.rumble_strength); constructor.Bind("gyro_sensitivity", &control_options_context.gyro_sensitivity); + constructor.Bind("mouse_sensitivity", &control_options_context.mouse_sensitivity); bind_option(constructor, "targeting_mode", &control_options_context.targeting_mode); bind_option(constructor, "background_input_mode", &control_options_context.background_input_mode);