mirror of
				https://github.com/Zelda64Recomp/Zelda64Recomp.git
				synced 2025-10-30 08:03:03 +00:00 
			
		
		
		
	 19d2e38499
			
		
	
	
		19d2e38499
		
			
		
	
	
	
	
		
			
			* feat: allow configuring menu accept/apply buttons * Update assets/icons/Reset.svg Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com> --------- Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
		
			
				
	
	
		
			887 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			887 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include <atomic>
 | |
| #include <mutex>
 | |
| 
 | |
| #include "ultramodern/ultramodern.hpp"
 | |
| #include "librecomp/recomp.h"
 | |
| #include "recomp_input.h"
 | |
| #include "zelda_config.h"
 | |
| #include "recomp_ui.h"
 | |
| #include "SDL.h"
 | |
| #include "promptfont.h"
 | |
| #include "GamepadMotion.hpp"
 | |
| 
 | |
| constexpr float axis_threshold = 0.5f;
 | |
| 
 | |
| struct ControllerState {
 | |
|     SDL_GameController* controller;
 | |
|     std::array<float, 3> latest_accelerometer;
 | |
|     GamepadMotion motion;
 | |
|     uint32_t prev_gyro_timestamp;
 | |
|     ControllerState() : controller{}, latest_accelerometer{}, motion{}, prev_gyro_timestamp{} {
 | |
|         motion.Reset();
 | |
|         motion.SetCalibrationMode(GamepadMotionHelpers::CalibrationMode::Stillness | GamepadMotionHelpers::CalibrationMode::SensorFusion);
 | |
|     };
 | |
| };
 | |
| 
 | |
| static struct {
 | |
|     const Uint8* keys = nullptr;
 | |
|     SDL_Keymod keymod = SDL_Keymod::KMOD_NONE;
 | |
|     int numkeys = 0;
 | |
|     std::atomic_int32_t mouse_wheel_pos = 0;
 | |
|     std::mutex cur_controllers_mutex;
 | |
|     std::vector<SDL_GameController*> cur_controllers{};
 | |
|     std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
 | |
|     
 | |
|     std::array<float, 2> rotation_delta{};
 | |
|     std::array<float, 2> mouse_delta{};
 | |
|     std::mutex pending_input_mutex;
 | |
|     std::array<float, 2> pending_rotation_delta{};
 | |
|     std::array<float, 2> pending_mouse_delta{};
 | |
| 
 | |
|     float cur_rumble;
 | |
|     bool rumble_active;
 | |
| } InputState;
 | |
| 
 | |
| std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
 | |
| std::atomic<recomp::InputField> scanned_input;
 | |
| 
 | |
| enum class InputType {
 | |
|     None = 0, // Using zero for None ensures that default initialized InputFields are unbound.
 | |
|     Keyboard,
 | |
|     Mouse,
 | |
|     ControllerDigital,
 | |
|     ControllerAnalog // Axis input_id values are the SDL value + 1
 | |
| };
 | |
| 
 | |
| void set_scanned_input(recomp::InputField value) {
 | |
|     scanning_device.store(recomp::InputDevice::COUNT);
 | |
|     scanned_input.store(value);
 | |
| }
 | |
| 
 | |
| recomp::InputField recomp::get_scanned_input() {
 | |
|     recomp::InputField ret = scanned_input.load();
 | |
|     scanned_input.store({});
 | |
|     return ret;
 | |
| }
 | |
| 
 | |
| void recomp::start_scanning_input(recomp::InputDevice device) {
 | |
|     scanned_input.store({});
 | |
|     scanning_device.store(device);
 | |
| }
 | |
| 
 | |
| void recomp::stop_scanning_input() {
 | |
|     scanning_device.store(recomp::InputDevice::COUNT);
 | |
| }
 | |
| 
 | |
| void queue_if_enabled(SDL_Event* event) {
 | |
|     if (!recomp::all_input_disabled()) {
 | |
|         recompui::queue_event(*event);
 | |
|     }
 | |
| }
 | |
| 
 | |
| static std::atomic_bool cursor_enabled = true;
 | |
| 
 | |
| void recompui::set_cursor_visible(bool visible) {
 | |
|     cursor_enabled.store(visible);
 | |
| }
 | |
| 
 | |
| bool should_override_keystate(SDL_Scancode key, SDL_Keymod mod) {
 | |
|     // Override Enter when Alt is held.
 | |
|     if (key == SDL_Scancode::SDL_SCANCODE_RETURN) {
 | |
|         if (mod & SDL_Keymod::KMOD_ALT) {
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return false;        
 | |
| }
 | |
| 
 | |
| bool sdl_event_filter(void* userdata, SDL_Event* event) {
 | |
|     switch (event->type) {
 | |
|     case SDL_EventType::SDL_KEYDOWN:
 | |
|         {
 | |
|             SDL_KeyboardEvent* keyevent = &event->key;
 | |
| 
 | |
|             // Skip repeated events when not in the menu
 | |
|             if (recompui::get_current_menu() == recompui::Menu::None &&
 | |
|                 event->key.repeat) {
 | |
|                 break;
 | |
|             }
 | |
| 
 | |
|             if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
 | |
|                 keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
 | |
|             ) {
 | |
|                 recompui::toggle_fullscreen();
 | |
|             }
 | |
|             if (scanning_device != recomp::InputDevice::COUNT) {
 | |
|                 if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
 | |
|                     recomp::cancel_scanning_input();
 | |
|                 } else if (scanning_device == recomp::InputDevice::Keyboard) {
 | |
|                     set_scanned_input({(uint32_t)InputType::Keyboard, keyevent->keysym.scancode});
 | |
|                 }
 | |
|             } else {
 | |
|                 if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
 | |
|                     queue_if_enabled(event);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         break;
 | |
|     case SDL_EventType::SDL_CONTROLLERDEVICEADDED:
 | |
|         {
 | |
|             SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
 | |
|             SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
 | |
|             printf("Controller added: %d\n", controller_event->which);
 | |
|             if (controller != nullptr) {
 | |
|                 printf("  Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
 | |
|                 ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
 | |
|                 state.controller = controller;
 | |
| 
 | |
|                 if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
 | |
|                     SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
 | |
|                     SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         break;
 | |
|     case SDL_EventType::SDL_CONTROLLERDEVICEREMOVED:
 | |
|         {
 | |
|             SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
 | |
|             printf("Controller removed: %d\n", controller_event->which);
 | |
|             InputState.controller_states.erase(controller_event->which);
 | |
|         }
 | |
|         break;
 | |
|     case SDL_EventType::SDL_QUIT: {
 | |
|         if (!ultramodern::is_game_started()) {
 | |
|             ultramodern::quit();
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         if (recompui::get_current_menu() != recompui::Menu::Config) {
 | |
|             recompui::set_current_menu(recompui::Menu::Config);
 | |
|         }
 | |
| 
 | |
|         zelda64::open_quit_game_prompt();
 | |
|         recompui::activate_mouse();
 | |
|         break;
 | |
|     }
 | |
|     case SDL_EventType::SDL_MOUSEWHEEL:
 | |
|         {
 | |
|             SDL_MouseWheelEvent* wheel_event = &event->wheel;    
 | |
|             InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
 | |
|         }
 | |
|         queue_if_enabled(event);
 | |
|         break;
 | |
|     case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
 | |
|         if (scanning_device != recomp::InputDevice::COUNT) {
 | |
|             auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
 | |
|             auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
 | |
|             // note - magic number: 0 is InputType::None
 | |
|             if ((menuToggleBinding0.input_type != 0 && event->cbutton.button == menuToggleBinding0.input_id) ||
 | |
|                 (menuToggleBinding1.input_type != 0 && event->cbutton.button == menuToggleBinding1.input_id)) {
 | |
|                 recomp::cancel_scanning_input();
 | |
|             } else if (scanning_device == recomp::InputDevice::Controller) {
 | |
|                 SDL_ControllerButtonEvent* button_event = &event->cbutton;
 | |
|                 auto scanned_input_index = recomp::get_scanned_input_index();
 | |
|                 if ((scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
 | |
|                      scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
 | |
|                      scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
 | |
|                      button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
 | |
|                      button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
 | |
|                      button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
 | |
|                      button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
 | |
|                     break;
 | |
|                 }
 | |
| 
 | |
|                 set_scanned_input({(uint32_t)InputType::ControllerDigital, button_event->button});
 | |
|             }
 | |
|         } else {
 | |
|             queue_if_enabled(event);
 | |
|         }
 | |
|         break;
 | |
|     case SDL_EventType::SDL_CONTROLLERAXISMOTION:
 | |
|         if (scanning_device == recomp::InputDevice::Controller) {
 | |
|             auto scanned_input_index = recomp::get_scanned_input_index();
 | |
|             if (scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
 | |
|                 scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
 | |
|                 scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) {
 | |
|                 break;
 | |
|             }
 | |
| 
 | |
|             SDL_ControllerAxisEvent* axis_event = &event->caxis;
 | |
|             float axis_value = axis_event->value * (1/32768.0f);
 | |
|             if (axis_value > axis_threshold) {
 | |
|                 SDL_Event set_stick_return_event;
 | |
|                 set_stick_return_event.type = SDL_USEREVENT;
 | |
|                 set_stick_return_event.user.code = axis_event->axis;
 | |
|                 set_stick_return_event.user.data1 = nullptr;
 | |
|                 set_stick_return_event.user.data2 = nullptr;
 | |
|                 recompui::queue_event(set_stick_return_event);
 | |
|                 
 | |
|                 set_scanned_input({(uint32_t)InputType::ControllerAnalog, axis_event->axis + 1});
 | |
|             }
 | |
|             else if (axis_value < -axis_threshold) {
 | |
|                 SDL_Event set_stick_return_event;
 | |
|                 set_stick_return_event.type = SDL_USEREVENT;
 | |
|                 set_stick_return_event.user.code = axis_event->axis;
 | |
|                 set_stick_return_event.user.data1 = nullptr;
 | |
|                 set_stick_return_event.user.data2 = nullptr;
 | |
|                 recompui::queue_event(set_stick_return_event);
 | |
| 
 | |
|                 set_scanned_input({(uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1});
 | |
|             }
 | |
|         } else {
 | |
|             queue_if_enabled(event);
 | |
|         }
 | |
|         break;
 | |
|     case SDL_EventType::SDL_CONTROLLERSENSORUPDATE:
 | |
|         if (event->csensor.sensor == SDL_SensorType::SDL_SENSOR_ACCEL) {
 | |
|             // Convert acceleration to g's.
 | |
|             float x = event->csensor.data[0] / SDL_STANDARD_GRAVITY;
 | |
|             float y = event->csensor.data[1] / SDL_STANDARD_GRAVITY;
 | |
|             float z = event->csensor.data[2] / SDL_STANDARD_GRAVITY;
 | |
|             ControllerState& state = InputState.controller_states[event->csensor.which];
 | |
|             state.latest_accelerometer[0] = x;
 | |
|             state.latest_accelerometer[1] = y;
 | |
|             state.latest_accelerometer[2] = z;
 | |
|         }
 | |
|         else if (event->csensor.sensor == SDL_SensorType::SDL_SENSOR_GYRO) {
 | |
|             // constexpr float gyro_threshold = 0.05f;
 | |
|             // Convert rotational velocity to degrees per second.
 | |
|             constexpr float rad_to_deg = 180.0f / M_PI;
 | |
|             float x = event->csensor.data[0] * rad_to_deg;
 | |
|             float y = event->csensor.data[1] * rad_to_deg;
 | |
|             float z = event->csensor.data[2] * rad_to_deg;
 | |
|             ControllerState& state = InputState.controller_states[event->csensor.which];
 | |
|             uint64_t cur_timestamp = event->csensor.timestamp;
 | |
|             uint32_t delta_ms = cur_timestamp - state.prev_gyro_timestamp;
 | |
|             state.motion.ProcessMotion(x, y, z, state.latest_accelerometer[0], state.latest_accelerometer[1], state.latest_accelerometer[2], delta_ms * 0.001f);
 | |
|             state.prev_gyro_timestamp = cur_timestamp;
 | |
| 
 | |
|             float rot_x = 0.0f;
 | |
|             float rot_y = 0.0f;
 | |
|             state.motion.GetPlayerSpaceGyro(rot_x, rot_y);
 | |
| 
 | |
|             {
 | |
|                 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;
 | |
|     }
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| void recomp::handle_events() {
 | |
|     SDL_Event cur_event;
 | |
|     static bool exited = false;
 | |
|     while (SDL_PollEvent(&cur_event) && !exited) {
 | |
|         exited = sdl_event_filter(nullptr, &cur_event);
 | |
| 
 | |
|         // 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);
 | |
|     }
 | |
| }
 | |
| 
 | |
| constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
 | |
| constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B;
 | |
| constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X;
 | |
| constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y;
 | |
| 
 | |
| const recomp::DefaultN64Mappings recomp::default_n64_keyboard_mappings = {
 | |
|     .a = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_SPACE}
 | |
|     },
 | |
|     .b = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LSHIFT}
 | |
|     },
 | |
|     .l = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_E}
 | |
|     },
 | |
|     .r = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_R}
 | |
|     },
 | |
|     .z = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_Q}
 | |
|     },
 | |
|     .start = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
 | |
|     },
 | |
|     .c_left = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LEFT}
 | |
|     },
 | |
|     .c_right = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RIGHT}
 | |
|     },
 | |
|     .c_up = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_UP}
 | |
|     },
 | |
|     .c_down = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_DOWN}
 | |
|     },
 | |
|     .dpad_left = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_J}
 | |
|     },
 | |
|     .dpad_right = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_L}
 | |
|     },
 | |
|     .dpad_up = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_I}
 | |
|     },
 | |
|     .dpad_down = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_K}
 | |
|     },
 | |
|     .analog_left = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_A}
 | |
|     },
 | |
|     .analog_right = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_D}
 | |
|     },
 | |
|     .analog_up = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_W}
 | |
|     },
 | |
|     .analog_down = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_S}
 | |
|     },
 | |
|     .toggle_menu = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_ESCAPE}
 | |
|     },
 | |
|     .accept_menu = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
 | |
|     },
 | |
|     .apply_menu = {
 | |
|         {.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_F}
 | |
|     }
 | |
| };
 | |
| 
 | |
| const recomp::DefaultN64Mappings recomp::default_n64_controller_mappings = {
 | |
|     .a = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
 | |
|     },
 | |
|     .b = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
 | |
|     },
 | |
|     .l = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
 | |
|     },
 | |
|     .r = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERRIGHT + 1},
 | |
|     },
 | |
|     .z = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERLEFT + 1},
 | |
|     },
 | |
|     .start = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START},
 | |
|     },
 | |
|     .c_left = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTX + 1)},
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_NORTH},
 | |
|     },
 | |
|     .c_right = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTX + 1},
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_EAST},
 | |
|     },
 | |
|     .c_up = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTY + 1)},
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSTICK},
 | |
|     },
 | |
|     .c_down = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTY + 1},
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
 | |
|     },
 | |
|     .dpad_left = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_LEFT},
 | |
|     },
 | |
|     .dpad_right = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
 | |
|     },
 | |
|     .dpad_up = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_UP},
 | |
|     },
 | |
|     .dpad_down = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_DOWN},
 | |
|     },
 | |
|     .analog_left = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTX + 1)},
 | |
|     },
 | |
|     .analog_right = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTX + 1},
 | |
|     },
 | |
|     .analog_up = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTY + 1)},
 | |
|     },
 | |
|     .analog_down = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTY + 1},
 | |
|     },
 | |
|     .toggle_menu = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_BACK},
 | |
|     },
 | |
|     .accept_menu = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
 | |
|     },
 | |
|     .apply_menu = {
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
 | |
|         {.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START}
 | |
|     }
 | |
| };
 | |
| 
 | |
| void recomp::poll_inputs() {
 | |
|     InputState.keys = SDL_GetKeyboardState(&InputState.numkeys);
 | |
|     InputState.keymod = SDL_GetModState();
 | |
| 
 | |
|     {
 | |
|         std::lock_guard lock{ InputState.cur_controllers_mutex };
 | |
|         InputState.cur_controllers.clear();
 | |
| 
 | |
|         for (const auto& [id, state] : InputState.controller_states) {
 | |
|             (void)id; // Avoid unused variable warning.
 | |
|             SDL_GameController* controller = state.controller;
 | |
|             if (controller != nullptr) {
 | |
|                 InputState.cur_controllers.push_back(controller);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Read the deltas while resetting them to zero.
 | |
|     {
 | |
|         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
 | |
|     // when restored, rather than allowing saving and loading at any point in time.
 | |
|     #if 0
 | |
|     if (InputState.keys) {
 | |
|         static bool save_was_held = false;
 | |
|         static bool load_was_held = false;
 | |
|         bool save_is_held = InputState.keys[SDL_SCANCODE_F5] != 0;
 | |
|         bool load_is_held = InputState.keys[SDL_SCANCODE_F7] != 0;
 | |
|         if (save_is_held && !save_was_held) {
 | |
|             zelda64::quicksave_save();
 | |
|         }
 | |
|         else if (load_is_held && !load_was_held) {
 | |
|             zelda64::quicksave_load();
 | |
|         }
 | |
|         save_was_held = save_is_held;
 | |
|     }
 | |
|     #endif
 | |
| }
 | |
| 
 | |
| void recomp::set_rumble(int controller_num, bool on) {
 | |
|     if (controller_num == 0) {
 | |
|         InputState.rumble_active = on;
 | |
|     }
 | |
| }
 | |
| 
 | |
| ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) {
 | |
|     switch (controller_num) {
 | |
|         case 0:
 | |
|             return ultramodern::input::connected_device_info_t {
 | |
|                 .connected_device = ultramodern::input::Device::Controller,
 | |
|                 .connected_pak = ultramodern::input::Pak::RumblePak,
 | |
|             };
 | |
|     }
 | |
| 
 | |
|     return ultramodern::input::connected_device_info_t {
 | |
|         .connected_device = ultramodern::input::Device::None,
 | |
|         .connected_pak = ultramodern::input::Pak::None,
 | |
|     };
 | |
| }
 | |
| 
 | |
| static float smoothstep(float from, float to, float amount) {
 | |
|     amount = (amount * amount) * (3.0f - 2.0f * amount);
 | |
|     return std::lerp(from, to, amount);
 | |
| }
 | |
| 
 | |
| // Update rumble to attempt to mimic the way n64 rumble ramps up and falls off
 | |
| void recomp::update_rumble() {
 | |
|     // Note: values are not accurate! just approximations based on feel
 | |
|     if (InputState.rumble_active) {
 | |
|         InputState.cur_rumble += 0.17f;
 | |
|         if (InputState.cur_rumble > 1) InputState.cur_rumble = 1;
 | |
|     } else {
 | |
|         InputState.cur_rumble *= 0.92f;
 | |
|         InputState.cur_rumble -= 0.01f;
 | |
|         if (InputState.cur_rumble < 0) InputState.cur_rumble = 0;
 | |
|     }
 | |
|     float smooth_rumble = smoothstep(0, 1, InputState.cur_rumble);
 | |
| 
 | |
|     uint16_t rumble_strength = smooth_rumble * (recomp::get_rumble_strength() * 0xFFFF / 100);
 | |
|     uint32_t duration = 1000000; // Dummy duration value that lasts long enough to matter as the game will reset rumble on its own.
 | |
|     {
 | |
|         std::lock_guard lock{ InputState.cur_controllers_mutex };
 | |
|         for (const auto& controller : InputState.cur_controllers) {
 | |
|             SDL_GameControllerRumble(controller, 0, rumble_strength, duration);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| bool controller_button_state(int32_t input_id) {
 | |
|     if (input_id >= 0 && input_id < SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MAX) {
 | |
|         SDL_GameControllerButton button = (SDL_GameControllerButton)input_id;
 | |
|         bool ret = false;
 | |
|         {
 | |
|             std::lock_guard lock{ InputState.cur_controllers_mutex };
 | |
|             for (const auto& controller : InputState.cur_controllers) {
 | |
|                 ret |= SDL_GameControllerGetButton(controller, button);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return ret;
 | |
|     }
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| static std::atomic_bool right_analog_suppressed = false;
 | |
| 
 | |
| float controller_axis_state(int32_t input_id, bool allow_suppression) {
 | |
|     if (abs(input_id) - 1 < SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_MAX) {
 | |
|         SDL_GameControllerAxis axis = (SDL_GameControllerAxis)(abs(input_id) - 1);
 | |
|         bool negative_range = input_id < 0;
 | |
|         float ret = 0.0f;
 | |
| 
 | |
|         {
 | |
|             std::lock_guard lock{ InputState.cur_controllers_mutex };
 | |
|             for (const auto& controller : InputState.cur_controllers) {
 | |
|                 float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1/32768.0f);
 | |
|                 if (negative_range) {
 | |
|                     cur_val = -cur_val;
 | |
|                 }
 | |
| 
 | |
|                 // Check if this input is a right analog axis and suppress it accordingly.
 | |
|                 if (allow_suppression && right_analog_suppressed.load() &&
 | |
|                     (axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX || axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY)) {
 | |
|                     cur_val = 0;
 | |
|                 }
 | |
|                 ret += std::clamp(cur_val, 0.0f, 1.0f);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return std::clamp(ret, 0.0f, 1.0f);
 | |
|     }
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| float recomp::get_input_analog(const recomp::InputField& field) {
 | |
|     switch ((InputType)field.input_type) {
 | |
|     case InputType::Keyboard:
 | |
|         if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
 | |
|             if (should_override_keystate(static_cast<SDL_Scancode>(field.input_id), InputState.keymod)) {
 | |
|                 return 0.0f;
 | |
|             }
 | |
|             return InputState.keys[field.input_id] ? 1.0f : 0.0f;
 | |
|         }
 | |
|         return 0.0f;
 | |
|     case InputType::ControllerDigital:
 | |
|         return controller_button_state(field.input_id) ? 1.0f : 0.0f;
 | |
|     case InputType::ControllerAnalog:
 | |
|         return controller_axis_state(field.input_id, true);
 | |
|     case InputType::Mouse:
 | |
|         // TODO mouse support
 | |
|         return 0.0f;
 | |
|     case InputType::None:
 | |
|         return false;
 | |
|     }
 | |
| }
 | |
| 
 | |
| float recomp::get_input_analog(const std::span<const recomp::InputField> fields) {
 | |
|     float ret = 0.0f;
 | |
|     for (const auto& field : fields) {
 | |
|         ret += get_input_analog(field);
 | |
|     }
 | |
|     return std::clamp(ret, 0.0f, 1.0f);
 | |
| }
 | |
| 
 | |
| bool recomp::get_input_digital(const recomp::InputField& field) {
 | |
|     switch ((InputType)field.input_type) {
 | |
|     case InputType::Keyboard:
 | |
|         if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
 | |
|             if (should_override_keystate(static_cast<SDL_Scancode>(field.input_id), InputState.keymod)) {
 | |
|                 return false;
 | |
|             }
 | |
|             return InputState.keys[field.input_id] != 0;
 | |
|         }
 | |
|         return false;
 | |
|     case InputType::ControllerDigital:
 | |
|         return controller_button_state(field.input_id);
 | |
|     case InputType::ControllerAnalog:
 | |
|         // TODO adjustable threshold
 | |
|         return controller_axis_state(field.input_id, true) >= axis_threshold;
 | |
|     case InputType::Mouse:
 | |
|         // TODO mouse support
 | |
|         return false;
 | |
|     case InputType::None:
 | |
|         return false;
 | |
|     }
 | |
| }
 | |
| 
 | |
| bool recomp::get_input_digital(const std::span<const recomp::InputField> fields) {
 | |
|     bool ret = 0;
 | |
|     for (const auto& field : fields) {
 | |
|         ret |= get_input_digital(field);
 | |
|     }
 | |
|     return ret;
 | |
| }
 | |
| 
 | |
| void recomp::get_gyro_deltas(float* x, float* y) {
 | |
|     std::array<float, 2> cur_rotation_delta = InputState.rotation_delta;
 | |
|     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<float, 2> 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;
 | |
| }
 | |
| 
 | |
| void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out) {
 | |
|     float joystick_deadzone = (float)recomp::get_joystick_deadzone() / 100.0f;
 | |
| 
 | |
|     if(fabsf(x_in) < joystick_deadzone) {
 | |
|         x_in = 0.0f;
 | |
|     }
 | |
|     else {
 | |
|         if(x_in > 0.0f) {
 | |
|             x_in -= joystick_deadzone;
 | |
|         } 
 | |
|         else {
 | |
|             x_in += joystick_deadzone;
 | |
|         }
 | |
| 
 | |
|         x_in /= (1.0f - joystick_deadzone);
 | |
|     }
 | |
| 
 | |
|     if(fabsf(y_in) < joystick_deadzone) {
 | |
|         y_in = 0.0f;
 | |
|     }
 | |
|     else {
 | |
|         if(y_in > 0.0f) {
 | |
|             y_in -= joystick_deadzone;
 | |
|         } 
 | |
|         else {
 | |
|             y_in += joystick_deadzone;
 | |
|         }
 | |
| 
 | |
|         y_in /= (1.0f - joystick_deadzone);
 | |
|     }
 | |
| 
 | |
|     *x_out = x_in;
 | |
|     *y_out = y_in;
 | |
| }
 | |
| 
 | |
| void recomp::get_right_analog(float* x, float* y) {
 | |
|     float x_val =
 | |
|         controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false) -
 | |
|         controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false);
 | |
|     float y_val =
 | |
|         controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false) -
 | |
|         controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false);
 | |
|     recomp::apply_joystick_deadzone(x_val, y_val, x, y);
 | |
| }
 | |
| 
 | |
| void recomp::set_right_analog_suppressed(bool suppressed) {
 | |
|     right_analog_suppressed.store(suppressed);
 | |
| }
 | |
| 
 | |
| bool recomp::game_input_disabled() {
 | |
|     // Disable input if any menu is open.
 | |
|     return recompui::get_current_menu() != recompui::Menu::None;
 | |
| }
 | |
| 
 | |
| bool recomp::all_input_disabled() {
 | |
|     // Disable all input if an input is being polled.
 | |
|     return scanning_device != recomp::InputDevice::COUNT;
 | |
| }
 | |
| 
 | |
| std::string controller_button_to_string(SDL_GameControllerButton button) {
 | |
|     switch (button) {
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_A:
 | |
|         return PF_GAMEPAD_A;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B:
 | |
|         return PF_GAMEPAD_B;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X:
 | |
|         return PF_GAMEPAD_X;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y:
 | |
|         return PF_GAMEPAD_Y;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_BACK:
 | |
|         return PF_XBOX_VIEW;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_GUIDE:
 | |
|         return PF_GAMEPAD_HOME;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_START:
 | |
|         return PF_XBOX_MENU;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSTICK:
 | |
|         return PF_ANALOG_L_CLICK;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSTICK:
 | |
|         return PF_ANALOG_R_CLICK;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
 | |
|         return PF_XBOX_LEFT_SHOULDER;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
 | |
|         return PF_XBOX_RIGHT_SHOULDER;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
 | |
|         return PF_DPAD_UP;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
 | |
|         return PF_DPAD_DOWN;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
 | |
|         return PF_DPAD_LEFT;
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
 | |
|         return PF_DPAD_RIGHT;
 | |
|     // case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
 | |
|     //     return "";
 | |
|     // case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
 | |
|     //     return "";
 | |
|     // case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
 | |
|     //     return "";
 | |
|     // case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
 | |
|     //     return "";
 | |
|     // case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
 | |
|     //     return "";
 | |
|     case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
 | |
|         return PF_SONY_TOUCHPAD;
 | |
|     default:
 | |
|         return "Button " + std::to_string(button);
 | |
|     }
 | |
| }
 | |
| 
 | |
| std::unordered_map<SDL_Scancode, std::string> scancode_codepoints {
 | |
|     {SDL_SCANCODE_LEFT, PF_KEYBOARD_LEFT},
 | |
|     // NOTE: UP and RIGHT are swapped with promptfont.
 | |
|     {SDL_SCANCODE_UP, PF_KEYBOARD_RIGHT},
 | |
|     {SDL_SCANCODE_RIGHT, PF_KEYBOARD_UP},
 | |
|     {SDL_SCANCODE_DOWN, PF_KEYBOARD_DOWN},
 | |
|     {SDL_SCANCODE_A, PF_KEYBOARD_A},
 | |
|     {SDL_SCANCODE_B, PF_KEYBOARD_B},
 | |
|     {SDL_SCANCODE_C, PF_KEYBOARD_C},
 | |
|     {SDL_SCANCODE_D, PF_KEYBOARD_D},
 | |
|     {SDL_SCANCODE_E, PF_KEYBOARD_E},
 | |
|     {SDL_SCANCODE_F, PF_KEYBOARD_F},
 | |
|     {SDL_SCANCODE_G, PF_KEYBOARD_G},
 | |
|     {SDL_SCANCODE_H, PF_KEYBOARD_H},
 | |
|     {SDL_SCANCODE_I, PF_KEYBOARD_I},
 | |
|     {SDL_SCANCODE_J, PF_KEYBOARD_J},
 | |
|     {SDL_SCANCODE_K, PF_KEYBOARD_K},
 | |
|     {SDL_SCANCODE_L, PF_KEYBOARD_L},
 | |
|     {SDL_SCANCODE_M, PF_KEYBOARD_M},
 | |
|     {SDL_SCANCODE_N, PF_KEYBOARD_N},
 | |
|     {SDL_SCANCODE_O, PF_KEYBOARD_O},
 | |
|     {SDL_SCANCODE_P, PF_KEYBOARD_P},
 | |
|     {SDL_SCANCODE_Q, PF_KEYBOARD_Q},
 | |
|     {SDL_SCANCODE_R, PF_KEYBOARD_R},
 | |
|     {SDL_SCANCODE_S, PF_KEYBOARD_S},
 | |
|     {SDL_SCANCODE_T, PF_KEYBOARD_T},
 | |
|     {SDL_SCANCODE_U, PF_KEYBOARD_U},
 | |
|     {SDL_SCANCODE_V, PF_KEYBOARD_V},
 | |
|     {SDL_SCANCODE_W, PF_KEYBOARD_W},
 | |
|     {SDL_SCANCODE_X, PF_KEYBOARD_X},
 | |
|     {SDL_SCANCODE_Y, PF_KEYBOARD_Y},
 | |
|     {SDL_SCANCODE_Z, PF_KEYBOARD_Z},
 | |
|     {SDL_SCANCODE_0, PF_KEYBOARD_0},
 | |
|     {SDL_SCANCODE_1, PF_KEYBOARD_1},
 | |
|     {SDL_SCANCODE_2, PF_KEYBOARD_2},
 | |
|     {SDL_SCANCODE_3, PF_KEYBOARD_3},
 | |
|     {SDL_SCANCODE_4, PF_KEYBOARD_4},
 | |
|     {SDL_SCANCODE_5, PF_KEYBOARD_5},
 | |
|     {SDL_SCANCODE_6, PF_KEYBOARD_6},
 | |
|     {SDL_SCANCODE_7, PF_KEYBOARD_7},
 | |
|     {SDL_SCANCODE_8, PF_KEYBOARD_8},
 | |
|     {SDL_SCANCODE_9, PF_KEYBOARD_9},
 | |
|     {SDL_SCANCODE_ESCAPE, PF_KEYBOARD_ESCAPE},
 | |
|     {SDL_SCANCODE_F1, PF_KEYBOARD_F1},
 | |
|     {SDL_SCANCODE_F2, PF_KEYBOARD_F2},
 | |
|     {SDL_SCANCODE_F3, PF_KEYBOARD_F3},
 | |
|     {SDL_SCANCODE_F4, PF_KEYBOARD_F4},
 | |
|     {SDL_SCANCODE_F5, PF_KEYBOARD_F5},
 | |
|     {SDL_SCANCODE_F6, PF_KEYBOARD_F6},
 | |
|     {SDL_SCANCODE_F7, PF_KEYBOARD_F7},
 | |
|     {SDL_SCANCODE_F8, PF_KEYBOARD_F8},
 | |
|     {SDL_SCANCODE_F9, PF_KEYBOARD_F9},
 | |
|     {SDL_SCANCODE_F10, PF_KEYBOARD_F10},
 | |
|     {SDL_SCANCODE_F11, PF_KEYBOARD_F11},
 | |
|     {SDL_SCANCODE_F12, PF_KEYBOARD_F12},
 | |
|     {SDL_SCANCODE_PRINTSCREEN, PF_KEYBOARD_PRINT_SCREEN},
 | |
|     {SDL_SCANCODE_SCROLLLOCK, PF_KEYBOARD_SCROLL_LOCK},
 | |
|     {SDL_SCANCODE_PAUSE, PF_KEYBOARD_PAUSE},
 | |
|     {SDL_SCANCODE_INSERT, PF_KEYBOARD_INSERT},
 | |
|     {SDL_SCANCODE_HOME, PF_KEYBOARD_HOME},
 | |
|     {SDL_SCANCODE_PAGEUP, PF_KEYBOARD_PAGE_UP},
 | |
|     {SDL_SCANCODE_DELETE, PF_KEYBOARD_DELETE},
 | |
|     {SDL_SCANCODE_END, PF_KEYBOARD_END},
 | |
|     {SDL_SCANCODE_PAGEDOWN, PF_KEYBOARD_PAGE_DOWN},
 | |
|     {SDL_SCANCODE_SPACE, PF_KEYBOARD_SPACE},
 | |
|     {SDL_SCANCODE_BACKSPACE, PF_KEYBOARD_BACKSPACE},
 | |
|     {SDL_SCANCODE_TAB, PF_KEYBOARD_TAB},
 | |
|     {SDL_SCANCODE_RETURN, PF_KEYBOARD_ENTER},
 | |
|     {SDL_SCANCODE_CAPSLOCK, PF_KEYBOARD_CAPS},
 | |
|     {SDL_SCANCODE_NUMLOCKCLEAR, PF_KEYBOARD_NUM_LOCK},
 | |
|     {SDL_SCANCODE_LSHIFT, "L" PF_KEYBOARD_SHIFT},
 | |
|     {SDL_SCANCODE_RSHIFT, "R" PF_KEYBOARD_SHIFT},
 | |
| };
 | |
| 
 | |
| std::string keyboard_input_to_string(SDL_Scancode key) {
 | |
|     if (scancode_codepoints.find(key) != scancode_codepoints.end()) {
 | |
|         return scancode_codepoints[key];
 | |
|     }
 | |
|     return std::to_string(key);
 | |
| }
 | |
| 
 | |
| std::string controller_axis_to_string(int axis) {
 | |
|     bool positive = axis > 0;
 | |
|     SDL_GameControllerAxis actual_axis = SDL_GameControllerAxis(abs(axis) - 1);
 | |
|     switch (actual_axis) {
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
 | |
|         return positive ? "\u21C0" : "\u21BC";
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
 | |
|         return positive ? "\u21C2" : "\u21BE";
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX:
 | |
|         return positive ? "\u21C1" : "\u21BD";
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY:
 | |
|         return positive ? "\u21C3" : "\u21BF";
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERLEFT:
 | |
|         return positive ? "\u2196" : "\u21DC";
 | |
|     case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
 | |
|         return positive ? "\u2197" : "\u21DD";
 | |
|     default:
 | |
|         return "Axis " + std::to_string(actual_axis) + (positive ? '+' : '-');
 | |
|     }
 | |
| }
 | |
| 
 | |
| std::string recomp::InputField::to_string() const {
 | |
|     switch ((InputType)input_type) {
 | |
|         case InputType::None:
 | |
|             return "";
 | |
|         case InputType::ControllerDigital:
 | |
|             return controller_button_to_string((SDL_GameControllerButton)input_id);
 | |
|         case InputType::ControllerAnalog:
 | |
|             return controller_axis_to_string(input_id);
 | |
|         case InputType::Keyboard:
 | |
|             return keyboard_input_to_string((SDL_Scancode)input_id);
 | |
|         default:
 | |
|             return std::to_string(input_type) + "," + std::to_string(input_id);
 | |
|     }
 | |
| }
 |