diff --git a/assets/icons/Keyboard.svg b/assets/icons/Keyboard.svg new file mode 100644 index 0000000..5f68e78 --- /dev/null +++ b/assets/icons/Keyboard.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/include/recomp_input.h b/include/recomp_input.h index a1f130f..7ceb044 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -13,6 +13,9 @@ #include "json/json.hpp" +#include "SDL.h" +#include "chrono" + namespace recomp { // x-macros to build input enums and arrays. // First parameter is the enum name, second parameter is the bit field for the input (or 0 if there is no associated one), third is the readable name. @@ -250,16 +253,33 @@ namespace recompinput { BindingState& get_binding_state(); int get_num_players(); - bool get_player_is_assigned(int player_index); - + + struct AssignedPlayer { + SDL_GameController* controller = nullptr; + bool is_assigned = false; + bool keyboard_enabled = false; + std::chrono::high_resolution_clock::duration last_button_press_timestamp = std::chrono::high_resolution_clock::duration::zero(); + + AssignedPlayer() : controller(nullptr), is_assigned(false), keyboard_enabled(false), last_button_press_timestamp(std::chrono::high_resolution_clock::duration::zero()) {}; + }; + + constexpr size_t temp_max_players = 4; + struct PlayerAssignmentState { bool is_assigning = false; int player_index = 0; + std::array temp_assigned_players; + + PlayerAssignmentState() : is_assigning(false), player_index(0), temp_assigned_players{} {}; }; + bool get_player_is_assigned(int player_index); void start_player_assignment(void); void stop_player_assignment(void); + void stop_player_assignment_and_close_modal(void); + void commit_player_assignment(void); bool is_player_assignment_active(); + std::chrono::steady_clock::duration get_player_time_since_last_button_press(int player_index); bool does_player_have_controller(int player_index); } diff --git a/src/game/input.cpp b/src/game/input.cpp index 2672a65..108ece5 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -24,12 +24,6 @@ struct ControllerState { }; }; -struct AssignedPlayer { - SDL_GameController* controller = nullptr; - bool is_assigned = false; - bool keyboard_enabled = false; -}; - static struct { const Uint8* keys = nullptr; SDL_Keymod keymod = SDL_Keymod::KMOD_NONE; @@ -38,7 +32,7 @@ static struct { std::mutex controllers_mutex; std::vector detected_controllers{}; std::vector detected_controller_options{}; - std::array assigned_controllers{}; // Only used when Multiplayer is enabled. + std::array assigned_controllers{}; // Only used when Multiplayer is enabled. std::unordered_map controller_states; bool single_controller = false; @@ -60,7 +54,7 @@ std::atomic scanning_device = recomp::InputDevice::COUNT; std::atomic scanned_input; static recompinput::BindingState binding_state; -static recompinput::PlayerAssignmentState player_assignment_state; +static recompinput::PlayerAssignmentState player_assignment_state{}; void recompinput::start_scanning_for_binding(int player_index, recomp::GameInput game_input, int binding_index) { binding_state.is_scanning = true; @@ -91,24 +85,36 @@ bool recompinput::get_player_is_assigned(int player_index) { return false; } - return InputState.assigned_controllers[player_index].is_assigned; + return player_assignment_state.temp_assigned_players[player_index].is_assigned; } void recompinput::start_player_assignment() { player_assignment_state.is_assigning = true; player_assignment_state.player_index = 0; - for (auto& player : InputState.assigned_controllers) { - player.controller = nullptr; - player.is_assigned = false; - player.keyboard_enabled = false; + for (auto& player : player_assignment_state.temp_assigned_players) { + player = AssignedPlayer{}; } } +static bool queue_close_player_assignment_modal = false; + void recompinput::stop_player_assignment() { player_assignment_state.is_assigning = false; player_assignment_state.player_index = -1; - recompui::assign_players_modal->close(); +} + +void recompinput::stop_player_assignment_and_close_modal() { + recompinput::stop_player_assignment(); + queue_close_player_assignment_modal = true; +} + +void recompinput::commit_player_assignment() { + recompinput::stop_player_assignment_and_close_modal(); + + for (int i = 0; i < recompinput::get_num_players(); i++) { + InputState.assigned_controllers[i] = player_assignment_state.temp_assigned_players[i]; + } } bool recompinput::is_player_assignment_active() { @@ -119,10 +125,22 @@ bool recompinput::does_player_have_controller(int player_index) { if (player_index < 0 || player_index >= recompinput::get_num_players()) { return false; } - return InputState.assigned_controllers[player_index].controller != nullptr; + return player_assignment_state.temp_assigned_players[player_index].controller != nullptr; +} + +std::chrono::steady_clock::duration recompinput::get_player_time_since_last_button_press(int player_index) { + if (player_index < 0 || player_index >= recompinput::get_num_players()) { + return std::chrono::steady_clock::duration::zero(); + } + return ultramodern::time_since_start() - player_assignment_state.temp_assigned_players[player_index].last_button_press_timestamp; } void process_player_assignment(SDL_Event* event) { + if (queue_close_player_assignment_modal) { + recompui::assign_players_modal->close(); + queue_close_player_assignment_modal = false; + } + if (!player_assignment_state.is_assigning) { return; } @@ -139,11 +157,18 @@ void process_player_assignment(SDL_Event* event) { recompinput::stop_player_assignment(); return; case SDL_Scancode::SDL_SCANCODE_SPACE: - InputState.assigned_controllers[player_assignment_state.player_index].is_assigned = true; - InputState.assigned_controllers[player_assignment_state.player_index].keyboard_enabled = true; + player_assignment_state.temp_assigned_players[player_assignment_state.player_index].is_assigned = true; + player_assignment_state.temp_assigned_players[player_assignment_state.player_index].keyboard_enabled = true; player_assignment_state.player_index++; printf("Assigned keyboard to player %d\n", player_assignment_state.player_index - 1); break; + default: + for (int i = 0; i < player_assignment_state.player_index; i++) { + if (player_assignment_state.temp_assigned_players[i].keyboard_enabled && player_assignment_state.temp_assigned_players[i].controller == nullptr) { + player_assignment_state.temp_assigned_players[i].last_button_press_timestamp = ultramodern::time_since_start(); + } + } + break; } break; } @@ -154,14 +179,18 @@ void process_player_assignment(SDL_Event* event) { bool can_be_mapped = true; for (int i = 0; i < player_assignment_state.player_index; i++) { - if (InputState.assigned_controllers[i].controller == controller_state.controller) { + if (player_assignment_state.temp_assigned_players[i].controller == controller_state.controller) { can_be_mapped = false; + player_assignment_state.temp_assigned_players[i].last_button_press_timestamp = ultramodern::time_since_start(); break; } } + if (can_be_mapped) { - InputState.assigned_controllers[player_assignment_state.player_index].is_assigned = true; - InputState.assigned_controllers[player_assignment_state.player_index].controller = controller_state.controller; + recompinput::AssignedPlayer& assigned_player = player_assignment_state.temp_assigned_players[player_assignment_state.player_index]; + assigned_player.is_assigned = true; + assigned_player.controller = controller_state.controller; + assigned_player.last_button_press_timestamp = ultramodern::time_since_start(); player_assignment_state.player_index++; printf("Assigned controller %d to player %d\n", joystick_id, player_assignment_state.player_index - 1); } diff --git a/src/ui/ui_assign_players_modal.cpp b/src/ui/ui_assign_players_modal.cpp index ccb6336..cee45d7 100644 --- a/src/ui/ui_assign_players_modal.cpp +++ b/src/ui/ui_assign_players_modal.cpp @@ -1,11 +1,84 @@ #include "ui_assign_players_modal.h" #include "elements/ui_label.h" +#include "elements/ui_container.h" #include "recomp_ui.h" namespace recompui { recompui::ContextId assign_players_modal_context; +static constexpr float assign_player_card_size = 128.0f; +static constexpr float assign_player_card_icon_size = 64.0f; +AssignPlayerCard::AssignPlayerCard(Element *parent) : Element(parent, 0, "div", false) { + set_display(Display::Flex); + set_flex_direction(FlexDirection::Column); + set_align_items(AlignItems::Center); + set_justify_content(JustifyContent::Center); + set_width(assign_player_card_size); + set_height(assign_player_card_size); + set_border_color(theme::color::BorderSoft); + set_border_width(theme::border::width, Unit::Dp); + set_border_radius(theme::border::radius_sm, Unit::Dp); + set_background_color(theme::color::Transparent); + + recompui::ContextId context = get_current_context(); + icon = context.create_element(this, "assets/icons/RecordBorder.svg"); + icon->set_width(assign_player_card_icon_size, Unit::Dp); + icon->set_height(assign_player_card_icon_size, Unit::Dp); + icon->set_color(theme::color::TextDim); +} + +AssignPlayerCard::~AssignPlayerCard() { +} + +void AssignPlayerCard::update_player_card(int player_index) { + static const float scale_anim_duration = 0.25f; + bool is_assigned = recompinput::get_player_is_assigned(player_index); + if (!is_assigned) { + icon->set_scale_2D(1.0f, 1.0f); + set_background_color(theme::color::Transparent); + icon->set_color(theme::color::TextDim); + icon->set_src("assets/icons/RecordBorder.svg"); + return; + } + + set_background_color(theme::color::PrimaryA20); + + bool has_controller =recompinput::does_player_have_controller(player_index); + + std::chrono::steady_clock::duration time_since_last_button_press = recompinput::get_player_time_since_last_button_press(player_index); + auto millis = static_cast(std::chrono::duration_cast(time_since_last_button_press).count()); + float seconds = millis / 1000.0f; + + if (seconds > 0 && seconds < scale_anim_duration) { + float t = 1.0f - (seconds / scale_anim_duration); + float scale = 1.0f + t * 0.15f; + icon->set_scale_2D(scale, scale); + icon->set_color(theme::color::Text, 200 + static_cast(t * 55.0f)); + } else { + icon->set_scale_2D(1.0f, 1.0f); + icon->set_color(theme::color::Text, 200); + } + + if (has_controller) { + icon->set_src("assets/icons/Cont.svg"); + } else { + icon->set_src("assets/icons/Keyboard.svg"); + } + +} + +static const float assignPlayersHFPaddingVert = 20.0f; +static const float assignPlayersHFPaddingHorz = 20.0f; + +static void set_button_side_styles(Element *el) { + el->set_align_items(AlignItems::Center); + el->set_width_auto(); + el->set_height_auto(); + el->set_flex_basis_auto(); + el->set_gap(8.0f); +} + AssignPlayersModal::AssignPlayersModal(Element *parent) : Element(parent, 0, "div", false) { recompui::ContextId context = get_current_context(); @@ -47,6 +120,12 @@ AssignPlayersModal::AssignPlayersModal(Element *parent) : Element(parent, 0, "di modal->set_border_color(theme::color::WhiteA20); modal->set_background_color(theme::color::ModalOverlay); + fake_focus_button = context.create_element(modal, 0, "button", false); + fake_focus_button->set_position(Position::Absolute); + fake_focus_button->set_width(0, Unit::Dp); + fake_focus_button->set_height(0, Unit::Dp); + fake_focus_button->set_opacity(0); + context.create_element