2023-11-24 22:10:21 +00:00
|
|
|
#include <atomic>
|
|
|
|
|
|
|
|
#include "../ultramodern/ultramodern.hpp"
|
|
|
|
#include "recomp.h"
|
|
|
|
#include "recomp_input.h"
|
|
|
|
#include "recomp_ui.h"
|
|
|
|
#include "SDL.h"
|
2023-12-17 21:32:13 +00:00
|
|
|
#include "rt64_layer.h"
|
2024-02-24 18:51:58 +00:00
|
|
|
#include "promptfont.h"
|
2024-01-23 04:08:59 +00:00
|
|
|
#include "GamepadMotion.hpp"
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2024-01-16 02:06:52 +00:00
|
|
|
constexpr float axis_threshold = 0.5f;
|
2023-12-13 07:06:56 +00:00
|
|
|
|
2024-01-23 04:08:59 +00:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
static struct {
|
|
|
|
const Uint8* keys = nullptr;
|
|
|
|
int numkeys = 0;
|
|
|
|
std::atomic_int32_t mouse_wheel_pos = 0;
|
|
|
|
std::vector<SDL_GameController*> cur_controllers{};
|
2024-01-23 04:08:59 +00:00
|
|
|
std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
|
|
|
|
std::array<float, 2> rotation_delta{};
|
|
|
|
std::mutex pending_rotation_mutex;
|
|
|
|
std::array<float, 2> pending_rotation_delta{};
|
2023-12-13 07:06:56 +00:00
|
|
|
} InputState;
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2024-01-16 02:06:52 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-03-12 13:35:58 +00:00
|
|
|
void recomp::stop_scanning_input() {
|
|
|
|
scanning_device.store(recomp::InputDevice::COUNT);
|
|
|
|
}
|
|
|
|
|
2024-01-16 02:06:52 +00:00
|
|
|
void queue_if_enabled(SDL_Event* event) {
|
|
|
|
if (!recomp::all_input_disabled()) {
|
|
|
|
recomp::queue_event(*event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-12 04:27:20 +00:00
|
|
|
static std::atomic_bool cursor_enabled = true;
|
|
|
|
|
|
|
|
void recomp::set_cursor_visible(bool visible) {
|
|
|
|
cursor_enabled.store(visible);
|
|
|
|
}
|
|
|
|
|
2023-11-24 22:10:21 +00:00
|
|
|
bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
|
|
|
switch (event->type) {
|
2023-12-17 21:32:13 +00:00
|
|
|
case SDL_EventType::SDL_KEYDOWN:
|
|
|
|
{
|
2024-01-16 02:06:52 +00:00
|
|
|
SDL_KeyboardEvent* keyevent = &event->key;
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2023-12-17 21:32:13 +00:00
|
|
|
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) {
|
2024-01-02 16:06:07 +00:00
|
|
|
RT64ChangeWindow();
|
2023-12-17 21:32:13 +00:00
|
|
|
}
|
2024-03-12 13:35:58 +00:00
|
|
|
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});
|
|
|
|
}
|
2024-03-04 17:50:11 +00:00
|
|
|
} else {
|
|
|
|
queue_if_enabled(event);
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
2023-12-17 21:32:13 +00:00
|
|
|
}
|
|
|
|
break;
|
2023-11-24 22:10:21 +00:00
|
|
|
case SDL_EventType::SDL_CONTROLLERDEVICEADDED:
|
|
|
|
{
|
2024-01-16 02:06:52 +00:00
|
|
|
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
2023-11-24 22:10:21 +00:00
|
|
|
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)));
|
2024-01-23 04:08:59 +00:00
|
|
|
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);
|
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case SDL_EventType::SDL_CONTROLLERDEVICEREMOVED:
|
|
|
|
{
|
2024-01-16 02:06:52 +00:00
|
|
|
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
2023-11-24 22:10:21 +00:00
|
|
|
printf("Controller removed: %d\n", controller_event->which);
|
2024-01-23 04:08:59 +00:00
|
|
|
InputState.controller_states.erase(controller_event->which);
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case SDL_EventType::SDL_QUIT:
|
|
|
|
ultramodern::quit();
|
|
|
|
return true;
|
|
|
|
case SDL_EventType::SDL_MOUSEWHEEL:
|
|
|
|
{
|
2024-01-16 02:06:52 +00:00
|
|
|
SDL_MouseWheelEvent* wheel_event = &event->wheel;
|
2023-12-13 07:06:56 +00:00
|
|
|
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
2024-01-16 02:06:52 +00:00
|
|
|
queue_if_enabled(event);
|
|
|
|
break;
|
|
|
|
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
|
2024-03-12 13:35:58 +00:00
|
|
|
if (scanning_device != recomp::InputDevice::COUNT) {
|
|
|
|
if (event->cbutton.button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_BACK) {
|
|
|
|
recomp::cancel_scanning_input();
|
|
|
|
} else if (scanning_device == recomp::InputDevice::Controller) {
|
|
|
|
SDL_ControllerButtonEvent* button_event = &event->cbutton;
|
|
|
|
set_scanned_input({(uint32_t)InputType::ControllerDigital, button_event->button});
|
|
|
|
}
|
2024-03-04 17:50:11 +00:00
|
|
|
} else {
|
|
|
|
queue_if_enabled(event);
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case SDL_EventType::SDL_CONTROLLERAXISMOTION:
|
|
|
|
if (scanning_device == recomp::InputDevice::Controller) {
|
|
|
|
SDL_ControllerAxisEvent* axis_event = &event->caxis;
|
|
|
|
float axis_value = axis_event->value * (1/32768.0f);
|
|
|
|
if (axis_value > axis_threshold) {
|
|
|
|
set_scanned_input({(uint32_t)InputType::ControllerAnalog, axis_event->axis + 1});
|
|
|
|
}
|
|
|
|
else if (axis_value < -axis_threshold) {
|
|
|
|
set_scanned_input({(uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1});
|
|
|
|
}
|
2024-03-04 17:50:11 +00:00
|
|
|
} else {
|
|
|
|
queue_if_enabled(event);
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
break;
|
2024-01-23 04:08:59 +00:00
|
|
|
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_rotation_mutex };
|
|
|
|
InputState.pending_rotation_delta[0] += rot_x;
|
|
|
|
InputState.pending_rotation_delta[1] += rot_y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2023-11-24 22:10:21 +00:00
|
|
|
default:
|
2024-01-16 02:06:52 +00:00
|
|
|
queue_if_enabled(event);
|
2023-11-24 22:10:21 +00:00
|
|
|
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);
|
2024-03-12 04:27:20 +00:00
|
|
|
SDL_ShowCursor(cursor_enabled ? SDL_ENABLE : SDL_DISABLE);
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
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;
|
|
|
|
|
2024-01-08 08:38:05 +00:00
|
|
|
const recomp::DefaultN64Mappings recomp::default_n64_keyboard_mappings = {
|
2023-12-13 07:06:56 +00:00
|
|
|
.a = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_SPACE}
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
2024-01-08 08:38:05 +00:00
|
|
|
.b = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LSHIFT}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.l = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_E}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.r = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_R}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.z = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_Q}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.start = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.c_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LEFT}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.c_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RIGHT}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.c_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_UP}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.c_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_DOWN}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.dpad_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_J}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.dpad_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_L}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.dpad_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_I}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.dpad_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_K}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.analog_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_A}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.analog_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_D}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.analog_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_W}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
.analog_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_S}
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const recomp::DefaultN64Mappings recomp::default_n64_controller_mappings = {
|
|
|
|
.a = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
|
2024-01-08 08:38:05 +00:00
|
|
|
},
|
2023-12-13 07:06:56 +00:00
|
|
|
.b = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_EAST},
|
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.l = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.r = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
|
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERRIGHT + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.z = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERLEFT + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.start = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.c_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTX + 1)},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.c_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTX + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.c_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTY + 1)},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.c_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTY + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.dpad_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_LEFT},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.dpad_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.dpad_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_UP},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.dpad_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_DOWN},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.analog_left = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTX + 1)},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.analog_right = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTX + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.analog_up = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTY + 1)},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
.analog_down = {
|
2024-01-16 02:06:52 +00:00
|
|
|
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTY + 1},
|
2023-12-13 07:06:56 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
void recomp::poll_inputs() {
|
|
|
|
InputState.keys = SDL_GetKeyboardState(&InputState.numkeys);
|
|
|
|
|
|
|
|
InputState.cur_controllers.clear();
|
|
|
|
|
2024-01-23 04:08:59 +00:00
|
|
|
for (const auto& [id, state] : InputState.controller_states) {
|
|
|
|
(void)id; // Avoid unused variable warning.
|
|
|
|
SDL_GameController* controller = state.controller;
|
2023-12-13 07:06:56 +00:00
|
|
|
if (controller != nullptr) {
|
|
|
|
InputState.cur_controllers.push_back(controller);
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-13 06:39:08 +00:00
|
|
|
|
2024-01-23 04:08:59 +00:00
|
|
|
// Read the deltas while resetting them to zero.
|
|
|
|
{
|
|
|
|
std::lock_guard lock{ InputState.pending_rotation_mutex };
|
|
|
|
InputState.rotation_delta = InputState.pending_rotation_delta;
|
|
|
|
InputState.pending_rotation_delta = { 0.0f, 0.0f };
|
|
|
|
}
|
|
|
|
|
2024-01-13 06:39:08 +00:00
|
|
|
// 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) {
|
|
|
|
recomp::quicksave_save();
|
|
|
|
}
|
|
|
|
else if (load_is_held && !load_was_held) {
|
|
|
|
recomp::quicksave_load();
|
|
|
|
}
|
|
|
|
save_was_held = save_is_held;
|
|
|
|
}
|
|
|
|
#endif
|
2023-12-13 07:06:56 +00:00
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2024-03-04 07:13:12 +00:00
|
|
|
void recomp::set_rumble(bool on) {
|
|
|
|
uint16_t rumble_strength = 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.
|
|
|
|
for (const auto& controller : InputState.cur_controllers) {
|
|
|
|
SDL_GameControllerRumble(controller, 0, on ? rumble_strength : 0, duration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
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;
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
for (const auto& controller : InputState.cur_controllers) {
|
|
|
|
ret |= SDL_GameControllerGetButton(controller, button);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
2023-12-13 07:06:56 +00:00
|
|
|
return false;
|
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
float controller_axis_state(int32_t input_id) {
|
|
|
|
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;
|
|
|
|
|
|
|
|
for (const auto& controller : InputState.cur_controllers) {
|
|
|
|
float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1/32768.0f);
|
|
|
|
if (negative_range) {
|
|
|
|
cur_val = -cur_val;
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
2023-12-13 07:06:56 +00:00
|
|
|
ret += std::clamp(cur_val, 0.0f, 1.0f);
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
return std::clamp(ret, 0.0f, 1.0f);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
float recomp::get_input_analog(const recomp::InputField& field) {
|
2024-01-16 02:06:52 +00:00
|
|
|
switch ((InputType)field.input_type) {
|
|
|
|
case InputType::Keyboard:
|
2023-12-13 07:06:56 +00:00
|
|
|
if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
|
|
|
|
return InputState.keys[field.input_id] ? 1.0f : 0.0f;
|
|
|
|
}
|
|
|
|
return 0.0f;
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::ControllerDigital:
|
2023-12-13 07:06:56 +00:00
|
|
|
return controller_button_state(field.input_id) ? 1.0f : 0.0f;
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::ControllerAnalog:
|
2023-12-13 07:06:56 +00:00
|
|
|
return controller_axis_state(field.input_id);
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::Mouse:
|
2023-12-13 07:06:56 +00:00
|
|
|
// TODO mouse support
|
|
|
|
return 0.0f;
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::None:
|
|
|
|
return false;
|
2023-12-13 07:06:56 +00:00
|
|
|
}
|
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
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);
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
bool recomp::get_input_digital(const recomp::InputField& field) {
|
2024-01-16 02:06:52 +00:00
|
|
|
switch ((InputType)field.input_type) {
|
|
|
|
case InputType::Keyboard:
|
2023-12-13 07:06:56 +00:00
|
|
|
if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
|
|
|
|
return InputState.keys[field.input_id] != 0;
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
2023-12-13 07:06:56 +00:00
|
|
|
return false;
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::ControllerDigital:
|
2023-12-13 07:06:56 +00:00
|
|
|
return controller_button_state(field.input_id);
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::ControllerAnalog:
|
2023-12-13 07:06:56 +00:00
|
|
|
// TODO adjustable threshold
|
2024-01-16 02:06:52 +00:00
|
|
|
return controller_axis_state(field.input_id) >= axis_threshold;
|
|
|
|
case InputType::Mouse:
|
2023-12-13 07:06:56 +00:00
|
|
|
// TODO mouse support
|
|
|
|
return false;
|
2024-01-16 02:06:52 +00:00
|
|
|
case InputType::None:
|
|
|
|
return false;
|
2023-12-13 07:06:56 +00:00
|
|
|
}
|
|
|
|
}
|
2023-11-24 22:10:21 +00:00
|
|
|
|
2023-12-13 07:06:56 +00:00
|
|
|
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;
|
2023-11-24 22:10:21 +00:00
|
|
|
}
|
2024-01-08 08:38:05 +00:00
|
|
|
|
2024-01-23 04:08:59 +00:00
|
|
|
void recomp::get_gyro_deltas(float* x, float* y) {
|
|
|
|
std::array<float, 2> cur_rotation_delta = InputState.rotation_delta;
|
|
|
|
*x = cur_rotation_delta[0];
|
|
|
|
*y = cur_rotation_delta[1];
|
|
|
|
}
|
|
|
|
|
2024-01-08 08:38:05 +00:00
|
|
|
bool recomp::game_input_disabled() {
|
|
|
|
// Disable input if any menu is open.
|
|
|
|
return recomp::get_current_menu() != recomp::Menu::None;
|
|
|
|
}
|
2024-01-16 02:06:52 +00:00
|
|
|
|
|
|
|
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 "\u21A7";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B:
|
|
|
|
return "\u21A6";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X:
|
|
|
|
return "\u21A4";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y:
|
|
|
|
return "\u21A5";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_BACK:
|
|
|
|
return "\u21FA";
|
|
|
|
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_GUIDE:
|
|
|
|
// return "";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_START:
|
|
|
|
return "\u21FB";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSTICK:
|
|
|
|
return "\u21BA";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSTICK:
|
|
|
|
return "\u21BB";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
|
|
|
|
return "\u2198";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
|
|
|
|
return "\u2199";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
|
|
|
|
return "\u219F";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
|
|
|
return "\u21A1";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
|
|
|
return "\u219E";
|
|
|
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
|
|
|
return "\u21A0";
|
|
|
|
// 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 "\u21E7";
|
2024-01-22 00:21:58 +00:00
|
|
|
default:
|
|
|
|
return "Button " + std::to_string(button);
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-24 18:51:58 +00:00
|
|
|
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},
|
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-01-16 02:06:52 +00:00
|
|
|
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 ? "\u219A" : "\u21DC";
|
|
|
|
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
|
|
|
return positive ? "\u219B" : "\u21DD";
|
2024-01-22 00:21:58 +00:00
|
|
|
default:
|
|
|
|
return "Axis " + std::to_string(actual_axis) + (positive ? '+' : '-');
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2024-02-24 18:51:58 +00:00
|
|
|
case InputType::Keyboard:
|
|
|
|
return keyboard_input_to_string((SDL_Scancode)input_id);
|
2024-01-22 00:21:58 +00:00
|
|
|
default:
|
|
|
|
return std::to_string(input_type) + "," + std::to_string(input_id);
|
2024-01-16 02:06:52 +00:00
|
|
|
}
|
|
|
|
}
|