early-access version 1938
This commit is contained in:
parent
3e7bea9718
commit
dd3c156af4
17 changed files with 155 additions and 101 deletions
|
@ -1,7 +1,7 @@
|
||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 1937.
|
This is the source code for early-access 1938.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,11 @@ enum class CPUAccuracy : u32 {
|
||||||
Unsafe = 2,
|
Unsafe = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class FullscreenMode : u32 {
|
||||||
|
Borderless = 0,
|
||||||
|
Exclusive = 1,
|
||||||
|
};
|
||||||
|
|
||||||
/** The BasicSetting class is a simple resource manager. It defines a label and default value
|
/** The BasicSetting class is a simple resource manager. It defines a label and default value
|
||||||
* alongside the actual value of the setting for simpler and less-error prone use with frontend
|
* alongside the actual value of the setting for simpler and less-error prone use with frontend
|
||||||
* configurations. Setting a default value and label is required, though subclasses may deviate from
|
* configurations. Setting a default value and label is required, though subclasses may deviate from
|
||||||
|
@ -322,11 +327,11 @@ struct Values {
|
||||||
Setting<u16> resolution_factor{1, "resolution_factor"};
|
Setting<u16> resolution_factor{1, "resolution_factor"};
|
||||||
// *nix platforms may have issues with the borderless windowed fullscreen mode.
|
// *nix platforms may have issues with the borderless windowed fullscreen mode.
|
||||||
// Default to exclusive fullscreen on these platforms for now.
|
// Default to exclusive fullscreen on these platforms for now.
|
||||||
Setting<int> fullscreen_mode{
|
Setting<FullscreenMode> fullscreen_mode{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
0,
|
FullscreenMode::Borderless,
|
||||||
#else
|
#else
|
||||||
1,
|
FullscreenMode::Exclusive,
|
||||||
#endif
|
#endif
|
||||||
"fullscreen_mode"};
|
"fullscreen_mode"};
|
||||||
Setting<int> aspect_ratio{0, "aspect_ratio"};
|
Setting<int> aspect_ratio{0, "aspect_ratio"};
|
||||||
|
|
|
@ -356,10 +356,10 @@ std::vector<std::unique_ptr<Polling::DevicePoller>> InputSubsystem::GetPollers([
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GenerateKeyboardParam(int key_code) {
|
std::string GenerateKeyboardParam(int key_code) {
|
||||||
Common::ParamPackage param{
|
Common::ParamPackage param;
|
||||||
{"engine", "keyboard"},
|
param.Set("engine", "keyboard");
|
||||||
{"code", std::to_string(key_code)},
|
param.Set("code", key_code);
|
||||||
};
|
param.Set("toggle", false);
|
||||||
return param.Serialize();
|
return param.Serialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ Common::ParamPackage MouseButtonFactory::GetNextInput() const {
|
||||||
if (pad.button != MouseInput::MouseButton::Undefined) {
|
if (pad.button != MouseInput::MouseButton::Undefined) {
|
||||||
params.Set("engine", "mouse");
|
params.Set("engine", "mouse");
|
||||||
params.Set("button", static_cast<u16>(pad.button));
|
params.Set("button", static_cast<u16>(pad.button));
|
||||||
|
params.Set("toggle", false);
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,12 @@ public:
|
||||||
state.buttons.insert_or_assign(button, value);
|
state.buttons.insert_or_assign(button, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PreSetButton(int button) {
|
||||||
|
if (!state.buttons.contains(button)) {
|
||||||
|
SetButton(button, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SetMotion(SDL_ControllerSensorEvent event) {
|
void SetMotion(SDL_ControllerSensorEvent event) {
|
||||||
constexpr float gravity_constant = 9.80665f;
|
constexpr float gravity_constant = 9.80665f;
|
||||||
std::lock_guard lock{mutex};
|
std::lock_guard lock{mutex};
|
||||||
|
@ -155,9 +161,16 @@ public:
|
||||||
state.axes.insert_or_assign(axis, value);
|
state.axes.insert_or_assign(axis, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
float GetAxis(int axis, float range) const {
|
void PreSetAxis(int axis) {
|
||||||
|
if (!state.axes.contains(axis)) {
|
||||||
|
SetAxis(axis, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetAxis(int axis, float range, float offset) const {
|
||||||
std::lock_guard lock{mutex};
|
std::lock_guard lock{mutex};
|
||||||
return static_cast<float>(state.axes.at(axis)) / (32767.0f * range);
|
const float value = static_cast<float>(state.axes.at(axis)) / 32767.0f;
|
||||||
|
return (value + offset) * range;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RumblePlay(u16 amp_low, u16 amp_high) {
|
bool RumblePlay(u16 amp_low, u16 amp_high) {
|
||||||
|
@ -174,9 +187,10 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<float, float> GetAnalog(int axis_x, int axis_y, float range) const {
|
std::tuple<float, float> GetAnalog(int axis_x, int axis_y, float range, float offset_x,
|
||||||
float x = GetAxis(axis_x, range);
|
float offset_y) const {
|
||||||
float y = GetAxis(axis_y, range);
|
float x = GetAxis(axis_x, range, offset_x);
|
||||||
|
float y = GetAxis(axis_y, range, offset_y);
|
||||||
y = -y; // 3DS uses an y-axis inverse from SDL
|
y = -y; // 3DS uses an y-axis inverse from SDL
|
||||||
|
|
||||||
// Make sure the coordinates are in the unit circle,
|
// Make sure the coordinates are in the unit circle,
|
||||||
|
@ -483,7 +497,7 @@ public:
|
||||||
trigger_if_greater(trigger_if_greater_) {}
|
trigger_if_greater(trigger_if_greater_) {}
|
||||||
|
|
||||||
bool GetStatus() const override {
|
bool GetStatus() const override {
|
||||||
const float axis_value = joystick->GetAxis(axis, 1.0f);
|
const float axis_value = joystick->GetAxis(axis, 1.0f, 0.0f);
|
||||||
if (trigger_if_greater) {
|
if (trigger_if_greater) {
|
||||||
return axis_value > threshold;
|
return axis_value > threshold;
|
||||||
}
|
}
|
||||||
|
@ -500,12 +514,14 @@ private:
|
||||||
class SDLAnalog final : public Input::AnalogDevice {
|
class SDLAnalog final : public Input::AnalogDevice {
|
||||||
public:
|
public:
|
||||||
explicit SDLAnalog(std::shared_ptr<SDLJoystick> joystick_, int axis_x_, int axis_y_,
|
explicit SDLAnalog(std::shared_ptr<SDLJoystick> joystick_, int axis_x_, int axis_y_,
|
||||||
bool invert_x_, bool invert_y_, float deadzone_, float range_)
|
bool invert_x_, bool invert_y_, float deadzone_, float range_,
|
||||||
|
float offset_x_, float offset_y_)
|
||||||
: joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), invert_x(invert_x_),
|
: joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), invert_x(invert_x_),
|
||||||
invert_y(invert_y_), deadzone(deadzone_), range(range_) {}
|
invert_y(invert_y_), deadzone(deadzone_), range(range_), offset_x(offset_x_),
|
||||||
|
offset_y(offset_y_) {}
|
||||||
|
|
||||||
std::tuple<float, float> GetStatus() const override {
|
std::tuple<float, float> GetStatus() const override {
|
||||||
auto [x, y] = joystick->GetAnalog(axis_x, axis_y, range);
|
auto [x, y] = joystick->GetAnalog(axis_x, axis_y, range, offset_x, offset_y);
|
||||||
const float r = std::sqrt((x * x) + (y * y));
|
const float r = std::sqrt((x * x) + (y * y));
|
||||||
if (invert_x) {
|
if (invert_x) {
|
||||||
x = -x;
|
x = -x;
|
||||||
|
@ -522,8 +538,8 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<float, float> GetRawStatus() const override {
|
std::tuple<float, float> GetRawStatus() const override {
|
||||||
const float x = joystick->GetAxis(axis_x, range);
|
const float x = joystick->GetAxis(axis_x, 1.0f, offset_x);
|
||||||
const float y = joystick->GetAxis(axis_y, range);
|
const float y = joystick->GetAxis(axis_y, 1.0f, offset_y);
|
||||||
return {x, -y};
|
return {x, -y};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,6 +571,8 @@ private:
|
||||||
const bool invert_y;
|
const bool invert_y;
|
||||||
const float deadzone;
|
const float deadzone;
|
||||||
const float range;
|
const float range;
|
||||||
|
const float offset_x;
|
||||||
|
const float offset_y;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SDLVibration final : public Input::VibrationDevice {
|
class SDLVibration final : public Input::VibrationDevice {
|
||||||
|
@ -621,7 +639,7 @@ public:
|
||||||
trigger_if_greater(trigger_if_greater_) {}
|
trigger_if_greater(trigger_if_greater_) {}
|
||||||
|
|
||||||
Input::MotionStatus GetStatus() const override {
|
Input::MotionStatus GetStatus() const override {
|
||||||
const float axis_value = joystick->GetAxis(axis, 1.0f);
|
const float axis_value = joystick->GetAxis(axis, 1.0f, 0.0f);
|
||||||
bool trigger = axis_value < threshold;
|
bool trigger = axis_value < threshold;
|
||||||
if (trigger_if_greater) {
|
if (trigger_if_greater) {
|
||||||
trigger = axis_value > threshold;
|
trigger = axis_value > threshold;
|
||||||
|
@ -720,13 +738,13 @@ public:
|
||||||
LOG_ERROR(Input, "Unknown direction {}", direction_name);
|
LOG_ERROR(Input, "Unknown direction {}", direction_name);
|
||||||
}
|
}
|
||||||
// This is necessary so accessing GetAxis with axis won't crash
|
// This is necessary so accessing GetAxis with axis won't crash
|
||||||
joystick->SetAxis(axis, 0);
|
joystick->PreSetAxis(axis);
|
||||||
return std::make_unique<SDLAxisButton>(joystick, axis, threshold, trigger_if_greater);
|
return std::make_unique<SDLAxisButton>(joystick, axis, threshold, trigger_if_greater);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int button = params.Get("button", 0);
|
const int button = params.Get("button", 0);
|
||||||
// This is necessary so accessing GetButton with button won't crash
|
// This is necessary so accessing GetButton with button won't crash
|
||||||
joystick->SetButton(button, false);
|
joystick->PreSetButton(button);
|
||||||
return std::make_unique<SDLButton>(joystick, button, toggle);
|
return std::make_unique<SDLButton>(joystick, button, toggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -757,13 +775,15 @@ public:
|
||||||
const std::string invert_y_value = params.Get("invert_y", "+");
|
const std::string invert_y_value = params.Get("invert_y", "+");
|
||||||
const bool invert_x = invert_x_value == "-";
|
const bool invert_x = invert_x_value == "-";
|
||||||
const bool invert_y = invert_y_value == "-";
|
const bool invert_y = invert_y_value == "-";
|
||||||
|
const float offset_x = params.Get("offset_x", 0.0f);
|
||||||
|
const float offset_y = params.Get("offset_y", 0.0f);
|
||||||
auto joystick = state.GetSDLJoystickByGUID(guid, port);
|
auto joystick = state.GetSDLJoystickByGUID(guid, port);
|
||||||
|
|
||||||
// This is necessary so accessing GetAxis with axis_x and axis_y won't crash
|
// This is necessary so accessing GetAxis with axis_x and axis_y won't crash
|
||||||
joystick->SetAxis(axis_x, 0);
|
joystick->PreSetAxis(axis_x);
|
||||||
joystick->SetAxis(axis_y, 0);
|
joystick->PreSetAxis(axis_y);
|
||||||
return std::make_unique<SDLAnalog>(joystick, axis_x, axis_y, invert_x, invert_y, deadzone,
|
return std::make_unique<SDLAnalog>(joystick, axis_x, axis_y, invert_x, invert_y, deadzone,
|
||||||
range);
|
range, offset_x, offset_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -844,13 +864,13 @@ public:
|
||||||
LOG_ERROR(Input, "Unknown direction {}", direction_name);
|
LOG_ERROR(Input, "Unknown direction {}", direction_name);
|
||||||
}
|
}
|
||||||
// This is necessary so accessing GetAxis with axis won't crash
|
// This is necessary so accessing GetAxis with axis won't crash
|
||||||
joystick->SetAxis(axis, 0);
|
joystick->PreSetAxis(axis);
|
||||||
return std::make_unique<SDLAxisMotion>(joystick, axis, threshold, trigger_if_greater);
|
return std::make_unique<SDLAxisMotion>(joystick, axis, threshold, trigger_if_greater);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int button = params.Get("button", 0);
|
const int button = params.Get("button", 0);
|
||||||
// This is necessary so accessing GetButton with button won't crash
|
// This is necessary so accessing GetButton with button won't crash
|
||||||
joystick->SetButton(button, false);
|
joystick->PreSetButton(button);
|
||||||
return std::make_unique<SDLButtonMotion>(joystick, button);
|
return std::make_unique<SDLButtonMotion>(joystick, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +1015,7 @@ Common::ParamPackage BuildButtonParamPackageForButton(int port, std::string guid
|
||||||
params.Set("port", port);
|
params.Set("port", port);
|
||||||
params.Set("guid", std::move(guid));
|
params.Set("guid", std::move(guid));
|
||||||
params.Set("button", button);
|
params.Set("button", button);
|
||||||
|
params.Set("toggle", false);
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1134,13 +1155,15 @@ Common::ParamPackage BuildParamPackageForBinding(int port, const std::string& gu
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::ParamPackage BuildParamPackageForAnalog(int port, const std::string& guid, int axis_x,
|
Common::ParamPackage BuildParamPackageForAnalog(int port, const std::string& guid, int axis_x,
|
||||||
int axis_y) {
|
int axis_y, float offset_x, float offset_y) {
|
||||||
Common::ParamPackage params;
|
Common::ParamPackage params;
|
||||||
params.Set("engine", "sdl");
|
params.Set("engine", "sdl");
|
||||||
params.Set("port", port);
|
params.Set("port", port);
|
||||||
params.Set("guid", guid);
|
params.Set("guid", guid);
|
||||||
params.Set("axis_x", axis_x);
|
params.Set("axis_x", axis_x);
|
||||||
params.Set("axis_y", axis_y);
|
params.Set("axis_y", axis_y);
|
||||||
|
params.Set("offset_x", offset_x);
|
||||||
|
params.Set("offset_y", offset_y);
|
||||||
params.Set("invert_x", "+");
|
params.Set("invert_x", "+");
|
||||||
params.Set("invert_y", "+");
|
params.Set("invert_y", "+");
|
||||||
return params;
|
return params;
|
||||||
|
@ -1342,24 +1365,39 @@ AnalogMapping SDLState::GetAnalogMappingForDevice(const Common::ParamPackage& pa
|
||||||
const auto& binding_left_y =
|
const auto& binding_left_y =
|
||||||
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
|
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
|
||||||
if (params.Has("guid2")) {
|
if (params.Has("guid2")) {
|
||||||
|
joystick2->PreSetAxis(binding_left_x.value.axis);
|
||||||
|
joystick2->PreSetAxis(binding_left_y.value.axis);
|
||||||
|
const auto left_offset_x = -joystick2->GetAxis(binding_left_x.value.axis, 1.0f, 0);
|
||||||
|
const auto left_offset_y = -joystick2->GetAxis(binding_left_y.value.axis, 1.0f, 0);
|
||||||
mapping.insert_or_assign(
|
mapping.insert_or_assign(
|
||||||
Settings::NativeAnalog::LStick,
|
Settings::NativeAnalog::LStick,
|
||||||
BuildParamPackageForAnalog(joystick2->GetPort(), joystick2->GetGUID(),
|
BuildParamPackageForAnalog(joystick2->GetPort(), joystick2->GetGUID(),
|
||||||
binding_left_x.value.axis, binding_left_y.value.axis));
|
binding_left_x.value.axis, binding_left_y.value.axis,
|
||||||
|
left_offset_x, left_offset_y));
|
||||||
} else {
|
} else {
|
||||||
|
joystick->PreSetAxis(binding_left_x.value.axis);
|
||||||
|
joystick->PreSetAxis(binding_left_y.value.axis);
|
||||||
|
const auto left_offset_x = -joystick->GetAxis(binding_left_x.value.axis, 1.0f, 0);
|
||||||
|
const auto left_offset_y = -joystick->GetAxis(binding_left_y.value.axis, 1.0f, 0);
|
||||||
mapping.insert_or_assign(
|
mapping.insert_or_assign(
|
||||||
Settings::NativeAnalog::LStick,
|
Settings::NativeAnalog::LStick,
|
||||||
BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
||||||
binding_left_x.value.axis, binding_left_y.value.axis));
|
binding_left_x.value.axis, binding_left_y.value.axis,
|
||||||
|
left_offset_x, left_offset_y));
|
||||||
}
|
}
|
||||||
const auto& binding_right_x =
|
const auto& binding_right_x =
|
||||||
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
|
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||||
const auto& binding_right_y =
|
const auto& binding_right_y =
|
||||||
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
|
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||||
|
joystick->PreSetAxis(binding_right_x.value.axis);
|
||||||
|
joystick->PreSetAxis(binding_right_y.value.axis);
|
||||||
|
const auto right_offset_x = -joystick->GetAxis(binding_right_x.value.axis, 1.0f, 0);
|
||||||
|
const auto right_offset_y = -joystick->GetAxis(binding_right_y.value.axis, 1.0f, 0);
|
||||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
|
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
|
||||||
BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
||||||
binding_right_x.value.axis,
|
binding_right_x.value.axis,
|
||||||
binding_right_y.value.axis));
|
binding_right_y.value.axis, right_offset_x,
|
||||||
|
right_offset_y));
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1563,8 +1601,9 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which)) {
|
if (const auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which)) {
|
||||||
|
// Set offset to zero since the joystick is not on center
|
||||||
auto params = BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
auto params = BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
|
||||||
first_axis, axis);
|
first_axis, axis, 0, 0);
|
||||||
first_axis = -1;
|
first_axis = -1;
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,12 +40,16 @@ constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_but
|
||||||
|
|
||||||
Tas::Tas() {
|
Tas::Tas() {
|
||||||
if (!Settings::values.tas_enable) {
|
if (!Settings::values.tas_enable) {
|
||||||
|
needs_reset = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LoadTasFiles();
|
LoadTasFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
Tas::~Tas() = default;
|
Tas::~Tas() {
|
||||||
|
SwapToStoredController();
|
||||||
|
is_running = false;
|
||||||
|
};
|
||||||
|
|
||||||
void Tas::LoadTasFiles() {
|
void Tas::LoadTasFiles() {
|
||||||
script_length = 0;
|
script_length = 0;
|
||||||
|
@ -184,6 +188,10 @@ std::string Tas::ButtonsToString(u32 button) const {
|
||||||
|
|
||||||
void Tas::UpdateThread() {
|
void Tas::UpdateThread() {
|
||||||
if (!Settings::values.tas_enable) {
|
if (!Settings::values.tas_enable) {
|
||||||
|
if (is_running) {
|
||||||
|
SwapToStoredController();
|
||||||
|
is_running = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,6 +302,9 @@ std::string Tas::WriteCommandButtons(u32 data) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tas::StartStop() {
|
void Tas::StartStop() {
|
||||||
|
if (!Settings::values.tas_enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
is_running = !is_running;
|
is_running = !is_running;
|
||||||
if (is_running) {
|
if (is_running) {
|
||||||
SwapToTasController();
|
SwapToTasController();
|
||||||
|
@ -330,25 +341,33 @@ void Tas::SwapToTasController() {
|
||||||
analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize();
|
analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is_old_input_saved = true;
|
||||||
Settings::values.is_device_reload_pending.store(true);
|
Settings::values.is_device_reload_pending.store(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tas::SwapToStoredController() {
|
void Tas::SwapToStoredController() {
|
||||||
if (!Settings::values.tas_swap_controllers) {
|
if (!is_old_input_saved) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto& players = Settings::values.players.GetValue();
|
auto& players = Settings::values.players.GetValue();
|
||||||
for (std::size_t index = 0; index < players.size(); index++) {
|
for (std::size_t index = 0; index < players.size(); index++) {
|
||||||
players[index] = player_mappings[index];
|
players[index] = player_mappings[index];
|
||||||
}
|
}
|
||||||
|
is_old_input_saved = false;
|
||||||
Settings::values.is_device_reload_pending.store(true);
|
Settings::values.is_device_reload_pending.store(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tas::Reset() {
|
void Tas::Reset() {
|
||||||
|
if (!Settings::values.tas_enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
needs_reset = true;
|
needs_reset = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Tas::Record() {
|
bool Tas::Record() {
|
||||||
|
if (!Settings::values.tas_enable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
is_recording = !is_recording;
|
is_recording = !is_recording;
|
||||||
return is_recording;
|
return is_recording;
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,6 +219,7 @@ private:
|
||||||
|
|
||||||
size_t script_length{0};
|
size_t script_length{0};
|
||||||
std::array<TasData, PLAYER_NUMBER> tas_data;
|
std::array<TasData, PLAYER_NUMBER> tas_data;
|
||||||
|
bool is_old_input_saved{false};
|
||||||
bool is_recording{false};
|
bool is_recording{false};
|
||||||
bool is_running{false};
|
bool is_running{false};
|
||||||
bool needs_reset{false};
|
bool needs_reset{false};
|
||||||
|
|
|
@ -1356,7 +1356,10 @@ void Config::SaveRendererValues() {
|
||||||
static_cast<u32>(Settings::values.renderer_backend.GetDefault()),
|
static_cast<u32>(Settings::values.renderer_backend.GetDefault()),
|
||||||
Settings::values.renderer_backend.UsingGlobal());
|
Settings::values.renderer_backend.UsingGlobal());
|
||||||
WriteGlobalSetting(Settings::values.vulkan_device);
|
WriteGlobalSetting(Settings::values.vulkan_device);
|
||||||
WriteGlobalSetting(Settings::values.fullscreen_mode);
|
WriteSetting(QString::fromStdString(Settings::values.fullscreen_mode.GetLabel()),
|
||||||
|
static_cast<u32>(Settings::values.fullscreen_mode.GetValue(global)),
|
||||||
|
static_cast<u32>(Settings::values.fullscreen_mode.GetDefault()),
|
||||||
|
Settings::values.fullscreen_mode.UsingGlobal());
|
||||||
WriteGlobalSetting(Settings::values.aspect_ratio);
|
WriteGlobalSetting(Settings::values.aspect_ratio);
|
||||||
WriteGlobalSetting(Settings::values.max_anisotropy);
|
WriteGlobalSetting(Settings::values.max_anisotropy);
|
||||||
WriteGlobalSetting(Settings::values.use_speed_limit);
|
WriteGlobalSetting(Settings::values.use_speed_limit);
|
||||||
|
|
|
@ -181,5 +181,6 @@ private:
|
||||||
// These metatype declarations cannot be in common/settings.h because core is devoid of QT
|
// These metatype declarations cannot be in common/settings.h because core is devoid of QT
|
||||||
Q_DECLARE_METATYPE(Settings::CPUAccuracy);
|
Q_DECLARE_METATYPE(Settings::CPUAccuracy);
|
||||||
Q_DECLARE_METATYPE(Settings::GPUAccuracy);
|
Q_DECLARE_METATYPE(Settings::GPUAccuracy);
|
||||||
|
Q_DECLARE_METATYPE(Settings::FullscreenMode);
|
||||||
Q_DECLARE_METATYPE(Settings::RendererBackend);
|
Q_DECLARE_METATYPE(Settings::RendererBackend);
|
||||||
Q_DECLARE_METATYPE(Settings::ShaderBackend);
|
Q_DECLARE_METATYPE(Settings::ShaderBackend);
|
||||||
|
|
|
@ -25,20 +25,6 @@ void ConfigurationShared::ApplyPerGameSetting(Settings::Setting<bool>* setting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurationShared::ApplyPerGameSetting(Settings::Setting<int>* setting,
|
|
||||||
const QComboBox* combobox) {
|
|
||||||
if (Settings::IsConfiguringGlobal() && setting->UsingGlobal()) {
|
|
||||||
setting->SetValue(combobox->currentIndex());
|
|
||||||
} else if (!Settings::IsConfiguringGlobal()) {
|
|
||||||
if (combobox->currentIndex() == ConfigurationShared::USE_GLOBAL_INDEX) {
|
|
||||||
setting->SetGlobal(true);
|
|
||||||
} else {
|
|
||||||
setting->SetGlobal(false);
|
|
||||||
setting->SetValue(combobox->currentIndex() - ConfigurationShared::USE_GLOBAL_OFFSET);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationShared::SetPerGameSetting(QCheckBox* checkbox,
|
void ConfigurationShared::SetPerGameSetting(QCheckBox* checkbox,
|
||||||
const Settings::Setting<bool>* setting) {
|
const Settings::Setting<bool>* setting) {
|
||||||
if (setting->UsingGlobal()) {
|
if (setting->UsingGlobal()) {
|
||||||
|
|
|
@ -28,7 +28,20 @@ enum class CheckState {
|
||||||
// ApplyPerGameSetting, given a Settings::Setting and a Qt UI element, properly applies a Setting
|
// ApplyPerGameSetting, given a Settings::Setting and a Qt UI element, properly applies a Setting
|
||||||
void ApplyPerGameSetting(Settings::Setting<bool>* setting, const QCheckBox* checkbox,
|
void ApplyPerGameSetting(Settings::Setting<bool>* setting, const QCheckBox* checkbox,
|
||||||
const CheckState& tracker);
|
const CheckState& tracker);
|
||||||
void ApplyPerGameSetting(Settings::Setting<int>* setting, const QComboBox* combobox);
|
template <typename Type>
|
||||||
|
void ApplyPerGameSetting(Settings::Setting<Type>* setting, const QComboBox* combobox) {
|
||||||
|
if (Settings::IsConfiguringGlobal() && setting->UsingGlobal()) {
|
||||||
|
setting->SetValue(static_cast<Type>(combobox->currentIndex()));
|
||||||
|
} else if (!Settings::IsConfiguringGlobal()) {
|
||||||
|
if (combobox->currentIndex() == ConfigurationShared::USE_GLOBAL_INDEX) {
|
||||||
|
setting->SetGlobal(true);
|
||||||
|
} else {
|
||||||
|
setting->SetGlobal(false);
|
||||||
|
setting->SetValue(static_cast<Type>(combobox->currentIndex() -
|
||||||
|
ConfigurationShared::USE_GLOBAL_OFFSET));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sets a Qt UI element given a Settings::Setting
|
// Sets a Qt UI element given a Settings::Setting
|
||||||
void SetPerGameSetting(QCheckBox* checkbox, const Settings::Setting<bool>* setting);
|
void SetPerGameSetting(QCheckBox* checkbox, const Settings::Setting<bool>* setting);
|
||||||
|
|
|
@ -65,6 +65,7 @@ void ConfigureCpu::UpdateGroup(int index) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureCpu::ApplyConfiguration() {
|
void ConfigureCpu::ApplyConfiguration() {
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.cpu_accuracy, ui->accuracy);
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.cpuopt_unsafe_unfuse_fma,
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.cpuopt_unsafe_unfuse_fma,
|
||||||
ui->cpuopt_unsafe_unfuse_fma,
|
ui->cpuopt_unsafe_unfuse_fma,
|
||||||
cpuopt_unsafe_unfuse_fma);
|
cpuopt_unsafe_unfuse_fma);
|
||||||
|
@ -80,22 +81,6 @@ void ConfigureCpu::ApplyConfiguration() {
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.cpuopt_unsafe_fastmem_check,
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.cpuopt_unsafe_fastmem_check,
|
||||||
ui->cpuopt_unsafe_fastmem_check,
|
ui->cpuopt_unsafe_fastmem_check,
|
||||||
cpuopt_unsafe_fastmem_check);
|
cpuopt_unsafe_fastmem_check);
|
||||||
|
|
||||||
if (Settings::IsConfiguringGlobal()) {
|
|
||||||
// Guard if during game and set to game-specific value
|
|
||||||
if (Settings::values.cpu_accuracy.UsingGlobal()) {
|
|
||||||
Settings::values.cpu_accuracy.SetValue(
|
|
||||||
static_cast<Settings::CPUAccuracy>(ui->accuracy->currentIndex()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ui->accuracy->currentIndex() == ConfigurationShared::USE_GLOBAL_INDEX) {
|
|
||||||
Settings::values.cpu_accuracy.SetGlobal(true);
|
|
||||||
} else {
|
|
||||||
Settings::values.cpu_accuracy.SetGlobal(false);
|
|
||||||
Settings::values.cpu_accuracy.SetValue(static_cast<Settings::CPUAccuracy>(
|
|
||||||
ui->accuracy->currentIndex() - ConfigurationShared::USE_GLOBAL_OFFSET));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureCpu::changeEvent(QEvent* event) {
|
void ConfigureCpu::changeEvent(QEvent* event) {
|
||||||
|
|
|
@ -98,7 +98,8 @@ void ConfigureGraphics::SetConfiguration() {
|
||||||
|
|
||||||
if (Settings::IsConfiguringGlobal()) {
|
if (Settings::IsConfiguringGlobal()) {
|
||||||
ui->api->setCurrentIndex(static_cast<int>(Settings::values.renderer_backend.GetValue()));
|
ui->api->setCurrentIndex(static_cast<int>(Settings::values.renderer_backend.GetValue()));
|
||||||
ui->fullscreen_mode_combobox->setCurrentIndex(Settings::values.fullscreen_mode.GetValue());
|
ui->fullscreen_mode_combobox->setCurrentIndex(
|
||||||
|
static_cast<int>(Settings::values.fullscreen_mode.GetValue()));
|
||||||
ui->aspect_ratio_combobox->setCurrentIndex(Settings::values.aspect_ratio.GetValue());
|
ui->aspect_ratio_combobox->setCurrentIndex(Settings::values.aspect_ratio.GetValue());
|
||||||
} else {
|
} else {
|
||||||
ConfigurationShared::SetPerGameSetting(ui->api, &Settings::values.renderer_backend);
|
ConfigurationShared::SetPerGameSetting(ui->api, &Settings::values.renderer_backend);
|
||||||
|
@ -310,8 +311,9 @@ void ConfigureGraphics::SetupPerGameUI() {
|
||||||
|
|
||||||
ConfigurationShared::SetColoredComboBox(ui->aspect_ratio_combobox, ui->ar_label,
|
ConfigurationShared::SetColoredComboBox(ui->aspect_ratio_combobox, ui->ar_label,
|
||||||
Settings::values.aspect_ratio.GetValue(true));
|
Settings::values.aspect_ratio.GetValue(true));
|
||||||
ConfigurationShared::SetColoredComboBox(ui->fullscreen_mode_combobox, ui->fullscreen_mode_label,
|
ConfigurationShared::SetColoredComboBox(
|
||||||
Settings::values.fullscreen_mode.GetValue(true));
|
ui->fullscreen_mode_combobox, ui->fullscreen_mode_label,
|
||||||
|
static_cast<int>(Settings::values.fullscreen_mode.GetValue(true)));
|
||||||
ConfigurationShared::InsertGlobalItem(
|
ConfigurationShared::InsertGlobalItem(
|
||||||
ui->api, static_cast<int>(Settings::values.renderer_backend.GetValue(true)));
|
ui->api, static_cast<int>(Settings::values.renderer_backend.GetValue(true)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,7 @@ void ConfigureGraphicsAdvanced::SetConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureGraphicsAdvanced::ApplyConfiguration() {
|
void ConfigureGraphicsAdvanced::ApplyConfiguration() {
|
||||||
// Subtract 2 if configuring per-game (separator and "use global configuration" take 2 slots)
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.gpu_accuracy, ui->gpu_accuracy);
|
||||||
const auto gpu_accuracy = static_cast<Settings::GPUAccuracy>(
|
|
||||||
ui->gpu_accuracy->currentIndex() -
|
|
||||||
((Settings::IsConfiguringGlobal()) ? 0 : ConfigurationShared::USE_GLOBAL_OFFSET));
|
|
||||||
|
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.max_anisotropy,
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.max_anisotropy,
|
||||||
ui->anisotropic_filtering_combobox);
|
ui->anisotropic_filtering_combobox);
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync, ui->use_vsync, use_vsync);
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync, ui->use_vsync, use_vsync);
|
||||||
|
@ -63,20 +59,6 @@ void ConfigureGraphicsAdvanced::ApplyConfiguration() {
|
||||||
use_caches_gc);
|
use_caches_gc);
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_fast_gpu_time,
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_fast_gpu_time,
|
||||||
ui->use_fast_gpu_time, use_fast_gpu_time);
|
ui->use_fast_gpu_time, use_fast_gpu_time);
|
||||||
|
|
||||||
if (Settings::IsConfiguringGlobal()) {
|
|
||||||
// Must guard in case of a during-game configuration when set to be game-specific.
|
|
||||||
if (Settings::values.gpu_accuracy.UsingGlobal()) {
|
|
||||||
Settings::values.gpu_accuracy.SetValue(gpu_accuracy);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ui->gpu_accuracy->currentIndex() == ConfigurationShared::USE_GLOBAL_INDEX) {
|
|
||||||
Settings::values.gpu_accuracy.SetGlobal(true);
|
|
||||||
} else {
|
|
||||||
Settings::values.gpu_accuracy.SetGlobal(false);
|
|
||||||
Settings::values.gpu_accuracy.SetValue(gpu_accuracy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureGraphicsAdvanced::changeEvent(QEvent* event) {
|
void ConfigureGraphicsAdvanced::changeEvent(QEvent* event) {
|
||||||
|
|
|
@ -323,11 +323,14 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
|
||||||
buttons_param[button_id].Clear();
|
buttons_param[button_id].Clear();
|
||||||
button_map[button_id]->setText(tr("[not set]"));
|
button_map[button_id]->setText(tr("[not set]"));
|
||||||
});
|
});
|
||||||
|
if (buttons_param[button_id].Has("toggle")) {
|
||||||
context_menu.addAction(tr("Toggle button"), [&] {
|
context_menu.addAction(tr("Toggle button"), [&] {
|
||||||
const bool toggle_value = !buttons_param[button_id].Get("toggle", false);
|
const bool toggle_value =
|
||||||
|
!buttons_param[button_id].Get("toggle", false);
|
||||||
buttons_param[button_id].Set("toggle", toggle_value);
|
buttons_param[button_id].Set("toggle", toggle_value);
|
||||||
button_map[button_id]->setText(ButtonToText(buttons_param[button_id]));
|
button_map[button_id]->setText(ButtonToText(buttons_param[button_id]));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (buttons_param[button_id].Has("threshold")) {
|
if (buttons_param[button_id].Has("threshold")) {
|
||||||
context_menu.addAction(tr("Set threshold"), [&] {
|
context_menu.addAction(tr("Set threshold"), [&] {
|
||||||
const int button_threshold = static_cast<int>(
|
const int button_threshold = static_cast<int>(
|
||||||
|
|
|
@ -1037,11 +1037,24 @@ void GMainWindow::InitializeHotkeys() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this),
|
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this),
|
||||||
&QShortcut::activated, this, [&] { input_subsystem->GetTas()->StartStop(); });
|
&QShortcut::activated, this, [&] {
|
||||||
|
if (!emulation_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input_subsystem->GetTas()->StartStop();
|
||||||
|
});
|
||||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this),
|
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this),
|
||||||
&QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); });
|
&QShortcut::activated, this, [&] {
|
||||||
|
if (emulation_running) {
|
||||||
|
input_subsystem->GetTas()->Reset();
|
||||||
|
}
|
||||||
|
input_subsystem->GetTas()->Reset();
|
||||||
|
});
|
||||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this),
|
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this),
|
||||||
&QShortcut::activated, this, [&] {
|
&QShortcut::activated, this, [&] {
|
||||||
|
if (!emulation_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
bool is_recording = input_subsystem->GetTas()->Record();
|
bool is_recording = input_subsystem->GetTas()->Record();
|
||||||
if (!is_recording) {
|
if (!is_recording) {
|
||||||
const auto res = QMessageBox::question(this, tr("TAS Recording"),
|
const auto res = QMessageBox::question(this, tr("TAS Recording"),
|
||||||
|
@ -1502,6 +1515,7 @@ void GMainWindow::ShutdownGame() {
|
||||||
}
|
}
|
||||||
game_list->SetFilterFocus();
|
game_list->SetFilterFocus();
|
||||||
tas_label->clear();
|
tas_label->clear();
|
||||||
|
input_subsystem->GetTas()->~Tas();
|
||||||
|
|
||||||
render_window->removeEventFilter(render_window);
|
render_window->removeEventFilter(render_window);
|
||||||
render_window->setAttribute(Qt::WA_Hover, false);
|
render_window->setAttribute(Qt::WA_Hover, false);
|
||||||
|
@ -2536,7 +2550,7 @@ void GMainWindow::ShowFullscreen() {
|
||||||
ui.menubar->hide();
|
ui.menubar->hide();
|
||||||
statusBar()->hide();
|
statusBar()->hide();
|
||||||
|
|
||||||
if (Settings::values.fullscreen_mode.GetValue() == 1) {
|
if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) {
|
||||||
showFullScreen();
|
showFullScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2551,7 +2565,7 @@ void GMainWindow::ShowFullscreen() {
|
||||||
} else {
|
} else {
|
||||||
UISettings::values.renderwindow_geometry = render_window->saveGeometry();
|
UISettings::values.renderwindow_geometry = render_window->saveGeometry();
|
||||||
|
|
||||||
if (Settings::values.fullscreen_mode.GetValue() == 1) {
|
if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) {
|
||||||
render_window->showFullScreen();
|
render_window->showFullScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2568,7 +2582,7 @@ void GMainWindow::ShowFullscreen() {
|
||||||
|
|
||||||
void GMainWindow::HideFullscreen() {
|
void GMainWindow::HideFullscreen() {
|
||||||
if (ui.action_Single_Window_Mode->isChecked()) {
|
if (ui.action_Single_Window_Mode->isChecked()) {
|
||||||
if (Settings::values.fullscreen_mode.GetValue() == 1) {
|
if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) {
|
||||||
showNormal();
|
showNormal();
|
||||||
restoreGeometry(UISettings::values.geometry);
|
restoreGeometry(UISettings::values.geometry);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2582,7 +2596,7 @@ void GMainWindow::HideFullscreen() {
|
||||||
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
|
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
|
||||||
ui.menubar->show();
|
ui.menubar->show();
|
||||||
} else {
|
} else {
|
||||||
if (Settings::values.fullscreen_mode.GetValue() == 1) {
|
if (Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive) {
|
||||||
render_window->showNormal();
|
render_window->showNormal();
|
||||||
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
|
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -124,7 +124,7 @@ void EmuWindow_SDL2::OnResize() {
|
||||||
|
|
||||||
void EmuWindow_SDL2::Fullscreen() {
|
void EmuWindow_SDL2::Fullscreen() {
|
||||||
switch (Settings::values.fullscreen_mode.GetValue()) {
|
switch (Settings::values.fullscreen_mode.GetValue()) {
|
||||||
case 1: // Exclusive fullscreen
|
case Settings::FullscreenMode::Exclusive:
|
||||||
// Set window size to render size before entering fullscreen -- SDL does not resize to
|
// Set window size to render size before entering fullscreen -- SDL does not resize to
|
||||||
// display dimensions in this mode.
|
// display dimensions in this mode.
|
||||||
// TODO: Multiply the window size by resolution_factor (for both docked modes)
|
// TODO: Multiply the window size by resolution_factor (for both docked modes)
|
||||||
|
@ -140,7 +140,7 @@ void EmuWindow_SDL2::Fullscreen() {
|
||||||
LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError());
|
LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError());
|
||||||
LOG_INFO(Frontend, "Attempting to use borderless fullscreen...");
|
LOG_INFO(Frontend, "Attempting to use borderless fullscreen...");
|
||||||
[[fallthrough]];
|
[[fallthrough]];
|
||||||
case 0: // Borderless window
|
case Settings::FullscreenMode::Borderless:
|
||||||
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) {
|
if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue