jak-project/game/system/hid/input_manager.h
Tyler Wilding d1a6c60eb8
game: disable keyboard input by default, give users a way to enable it via the imgui menu (#3295)
It was narrowed down recently that a lot of people have issues with the
controller input because of Steam Input working as intended. Steam Input
can be configured to replicate controller inputs as keyboard inputs (for
example, pressing X on your controller presses Enter on the keyboard).

This results in the problem of "jumping pauses the game" and similar
issues. This is a consequence of the intended behaviour of the game
listening to all input sources at the same time.

Since the vast majority of players are using controllers over keyboards,
it makes sense to disable the keyboard input by default to solve this
problem. However that makes things awkward for users that want to use
the keyboard (how do they enable the setting). The solution is a new
imgui option in the settings menu:
![Screenshot 2024-01-07
141224](https://github.com/open-goal/jak-project/assets/13153231/6f9ffa2d-be7a-433d-b698-15b70210e97e)

**Known issue that I don't care about** -- in Jak 1's menu code, since
the flags are controlled by pointers to values instead of a lambda like
in jak 2, the menu won't update live with the imgui option. This has no
functional impact and I don't care enough to fix it.

I also made the pc-settings.gc file persist on first load if the file
wasn't found. Hopefully this helps diagnose the support issues related
to the black screen.

# Why not just ignore the keyboard inputs for a period of time?

This won't work, the keyboard is polled every frame. Therefore if you
hold down the X button on your controller, steam is continuously
signaling that `Enter` is held down on the keyboard.

Yes it would be possible to completely disable the keyboard while the
controller is being used, but this defeats the purpose of creating an
input system that allows multiple input sources at the same time.

With an explicit option, not only can the user decide the behaviour they
want (do they want the keyboard ignored or simultaneously listened to)
but we avoid breaking strange edge-cases in usage leading to never
ending complexity:
- ie. imagine steam input sends events to the mouse, well you can't
disable the mouse while using the keyboard because most times people are
using mouse and keyboard
- ie. a user that wants to hold a direction with the keyboard and press
buttons on the controller in tandem (something i frequently do while
TAS'ing, to move in a perfect straight line)
2024-02-23 18:19:07 -05:00

157 lines
6.5 KiB
C++

#pragma once
#include <array>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <unordered_map>
#include <variant>
#include "common/common_types.h"
#include "devices/game_controller.h"
#include "devices/keyboard.h"
#include "devices/mouse.h"
#include "game/settings/settings.h"
#include "game/system/hid/input_bindings.h"
#include "third-party/SDL/include/SDL.h"
/// Central class that:
/// - keeps track of available input devices
/// - polls data from the input devices considered active
/// - fetches said data to be sent to the game
class InputManager {
private:
enum class EEInputEventType {
IGNORE_BACKGROUND_CONTROLLER_EVENTS,
UPDATE_RUMBLE,
SET_CONTROLLER_LED,
UPDATE_MOUSE_OPTIONS,
SET_AUTO_HIDE_MOUSE
};
struct EEInputEvent {
EEInputEventType type;
std::variant<bool, int> param1;
std::variant<bool, u8> param2;
std::variant<bool, u8> param3;
std::variant<u8> param4;
};
public:
InputManager();
~InputManager();
// Propagate and handle the SDL event, ignored it if it's not relevant
void process_sdl_event(const SDL_Event& event);
void poll_keyboard_data();
void clear_keyboard_actions();
void poll_mouse_data();
void clear_mouse_actions();
// Any cleanup that should happen after polling has completed for this frame
void finish_polling();
/// Any event coming from the EE thread that interacts directly with SDL should be enqueued as an
/// event so it can be ran from the proper thread context (the graphics thread)
void process_ee_events();
void register_command(const CommandBinding::Source source, const CommandBinding bind);
std::optional<std::shared_ptr<PadData>> get_current_data(const int port) const;
std::pair<int, int> get_mouse_pos() const { return m_mouse.get_mouse_pos(); }
MouseDevice::MouseButtonStatus get_mouse_button_status() const {
return m_mouse.get_mouse_button_status();
}
// These functions can be called from the EE and interact with SDL directly
// These should be enqueued in the event that for example, you try to set
// the controller LED while the controller is disconnecting
void enqueue_ignore_background_controller_events(const bool ignore);
void enqueue_update_rumble(const int port, const u8 low_intensity, const u8 high_intensity);
void enqueue_set_controller_led(const int port, const u8 red, const u8 green, const u8 blue);
void enqueue_update_mouse_options(const bool enabled,
const bool control_camera,
const bool control_movement);
void enqueue_set_auto_hide_mouse(const bool auto_hide_mouse);
// These functions can be called from the EE but they only interact with
// the InputManager, so it shouldn't hurt (they don't need to be enqueued)
// Also these are generally waiting for a response and...OpenGOAL does not have an async/await
// pattern!
int get_num_controllers() const { return m_available_controllers.size(); }
std::string get_controller_name(const int controller_id);
std::string get_current_bind(const int port,
const InputDeviceType device_type,
const bool buttons,
const int input_idx,
const bool analog_for_minimum);
int get_controller_index(const int port);
void set_controller_for_port(const int controller_id, const int port);
bool controller_has_led(const int port);
bool controller_has_rumble(const int port);
void enable_keyboard(const bool enabled);
bool get_waiting_for_bind() const { return m_waiting_for_bind.has_value(); }
void set_wait_for_bind(const InputDeviceType device_type,
const bool for_analog,
const bool for_minimum_analog,
const int input_idx);
void stop_waiting_for_bind() { m_waiting_for_bind = std::nullopt; }
void set_camera_sens(const float xsens, const float ysens);
void reset_input_bindings_to_defaults(const int port, const InputDeviceType device_type);
bool auto_hiding_cursor() { return m_auto_hide_mouse || m_mouse.is_camera_being_controlled(); }
void hide_cursor(const bool hide_cursor);
bool is_keyboard_enabled() {
return m_settings->keyboard_enabled || m_settings->keyboard_temp_enabled;
}
private:
std::mutex m_event_queue_mtx;
std::queue<EEInputEvent> ee_event_queue;
std::shared_ptr<game_settings::InputSettings> m_settings;
/// This data can be shared throughout the runtime, it is the current state of the
/// aggregate of all input sources for the given port
std::unordered_map<int, std::shared_ptr<PadData>> m_data;
/// A list of all the currently connected controllers that we can potentially read data
/// from if they are mapped to a given port
std::vector<std::shared_ptr<GameController>> m_available_controllers;
/// You can have many keyboards plugged into a computer, but we do not differentiate
/// between them it's all aggregated under one device.
KeyboardDevice m_keyboard;
/// You can have many mice plugged into a computer, but we do not differentiate between
/// them it's all aggregated under one device.
MouseDevice m_mouse;
/// A mapping between port numbers and the controller index. Connect as many controllers as
/// you want.
std::unordered_map<int, int> m_controller_port_mapping;
/// The port that the keyboard and mouse will be used for PadData
int m_keyboard_and_mouse_port = 0;
/// Collection of arbitrary commands to run on user actions
CommandBindingGroups m_command_binds;
bool m_mouse_enabled = false;
int m_skip_polling_for_n_frames = 0;
bool m_auto_hide_mouse = true;
bool m_mouse_currently_hidden = false;
bool m_ignore_background_controller_events = false;
/// No inputs will be processed while in this mode the first input detected from the relevant
/// device type will be used to set the bind and clear the flag
///
/// The game will poll for the status of this flag to know if a bind has been assigned
std::optional<InputBindAssignmentMeta> m_waiting_for_bind = std::nullopt;
void refresh_device_list();
void clear_inputs();
void ignore_background_controller_events(const bool ignore);
int update_rumble(const int port, const u8 low_intensity, const u8 high_intensity);
void set_controller_led(const int port, const u8 red, const u8 green, const u8 blue);
void update_mouse_options(const bool enabled,
const bool control_camera,
const bool control_movement);
void set_auto_hide_mouse(const bool auto_hide_mouse);
};