From 5cafa70d3b7f24881b578d2d473dc993fc47364b Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sun, 27 Sep 2020 11:18:07 -0400 Subject: applets/controller: Auto accept a valid single player configuration --- src/yuzu/applets/controller.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index c6fa3e4f6..ee770f315 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -229,6 +229,13 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QtControllerSelectorDialog::ApplyConfiguration); + // Enhancement: Check if the parameters have already been met before disconnecting controllers. + // If all the parameters are met AND only allows a single player, + // stop the constructor here as we do not need to continue. + if (CheckIfParametersMet() && parameters.enable_single_mode) { + return; + } + // If keep_controllers_connected is false, forcefully disconnect all controllers if (!parameters.keep_controllers_connected) { for (auto player : player_groupboxes) { @@ -236,13 +243,18 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( } } - CheckIfParametersMet(); - resize(0, 0); } QtControllerSelectorDialog::~QtControllerSelectorDialog() = default; +int QtControllerSelectorDialog::exec() { + if (parameters_met && parameters.enable_single_mode) { + return QDialog::Accepted; + } + return QDialog::exec(); +} + void QtControllerSelectorDialog::ApplyConfiguration() { // Update the controller state once more, just to be sure they are properly applied. for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { @@ -287,7 +299,7 @@ void QtControllerSelectorDialog::CallConfigureInputDialog() { CheckIfParametersMet(); } -void QtControllerSelectorDialog::CheckIfParametersMet() { +bool QtControllerSelectorDialog::CheckIfParametersMet() { // Here, we check and validate the current configuration against all applicable parameters. const auto num_connected_players = static_cast( std::count_if(player_groupboxes.begin(), player_groupboxes.end(), @@ -301,7 +313,7 @@ void QtControllerSelectorDialog::CheckIfParametersMet() { num_connected_players > max_supported_players) { parameters_met = false; ui->buttonBox->setEnabled(parameters_met); - return; + return parameters_met; } // Next, check against all connected controllers. @@ -326,14 +338,9 @@ void QtControllerSelectorDialog::CheckIfParametersMet() { return true; }(); - if (!all_controllers_compatible) { - parameters_met = false; - ui->buttonBox->setEnabled(parameters_met); - return; - } - - parameters_met = true; + parameters_met = all_controllers_compatible; ui->buttonBox->setEnabled(parameters_met); + return parameters_met; } void QtControllerSelectorDialog::SetSupportedControllers() { -- cgit v1.2.3 From 8f2959f6804e0d1048ecaa6f4046622e069fe7db Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Mon, 28 Sep 2020 10:00:15 -0400 Subject: settings: Preparation for per-game input settings --- src/yuzu/applets/controller.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index ee770f315..7697fe434 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -261,26 +261,26 @@ void QtControllerSelectorDialog::ApplyConfiguration() { UpdateControllerState(index); } - const bool pre_docked_mode = Settings::values.use_docked_mode; - Settings::values.use_docked_mode = ui->radioDocked->isChecked(); - OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode); + const bool pre_docked_mode = Settings::values.use_docked_mode.GetValue(); + Settings::values.use_docked_mode.SetValue(ui->radioDocked->isChecked()); + OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); - Settings::values.vibration_enabled = ui->vibrationGroup->isChecked(); + Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked()); } void QtControllerSelectorDialog::LoadConfiguration() { for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { - const auto connected = Settings::values.players[index].connected || - (index == 0 && Settings::values.players[8].connected); + const auto connected = Settings::values.players.GetValue()[index].connected || + (index == 0 && Settings::values.players.GetValue()[8].connected); player_groupboxes[index]->setChecked(connected); connected_controller_checkboxes[index]->setChecked(connected); emulated_controllers[index]->setCurrentIndex( - GetIndexFromControllerType(Settings::values.players[index].controller_type)); + GetIndexFromControllerType(Settings::values.players.GetValue()[index].controller_type)); } - UpdateDockedState(Settings::values.players[8].connected); + UpdateDockedState(Settings::values.players.GetValue()[8].connected); - ui->vibrationGroup->setChecked(Settings::values.vibration_enabled); + ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); } void QtControllerSelectorDialog::CallConfigureInputDialog() { @@ -448,7 +448,7 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) } void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) { - auto& player = Settings::values.players[player_index]; + auto& player = Settings::values.players.GetValue()[player_index]; player.controller_type = GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()); @@ -461,7 +461,7 @@ void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) } // Player 1 and Handheld - auto& handheld = Settings::values.players[8]; + auto& handheld = Settings::values.players.GetValue()[8]; // If Handheld is selected, copy all the settings from Player 1 to Handheld. if (player.controller_type == Settings::ControllerType::Handheld) { handheld = player; @@ -527,8 +527,8 @@ void QtControllerSelectorDialog::UpdateDockedState(bool is_handheld) { ui->radioDocked->setEnabled(!is_handheld); ui->radioUndocked->setEnabled(!is_handheld); - ui->radioDocked->setChecked(Settings::values.use_docked_mode); - ui->radioUndocked->setChecked(!Settings::values.use_docked_mode); + ui->radioDocked->setChecked(Settings::values.use_docked_mode.GetValue()); + ui->radioUndocked->setChecked(!Settings::values.use_docked_mode.GetValue()); // Also force into undocked mode if the controller type is handheld. if (is_handheld) { @@ -571,8 +571,8 @@ void QtControllerSelectorDialog::DisableUnsupportedPlayers() { for (std::size_t index = max_supported_players; index < NUM_PLAYERS; ++index) { // Disconnect any unsupported players here and disable or hide them if applicable. - Settings::values.players[index].connected = false; - UpdateController(Settings::values.players[index].controller_type, index, false); + Settings::values.players.GetValue()[index].connected = false; + UpdateController(Settings::values.players.GetValue()[index].controller_type, index, false); // Hide the player widgets when max_supported_controllers is less than or equal to 4. if (max_supported_players <= 4) { player_widgets[index]->hide(); -- cgit v1.2.3 From 652d6766d55acec6416dccb900a45c6660a86607 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Thu, 8 Oct 2020 23:43:07 -0400 Subject: configure_input: Hook up the vibration percentage spinbox This allows setting the vibration strength percentage anywhere from 1% to 100%. Also hooks up the remaining motion button and checkbox in the Controller Applet. --- src/yuzu/applets/controller.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 7697fe434..196b4f163 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -266,6 +266,8 @@ void QtControllerSelectorDialog::ApplyConfiguration() { OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked()); + Settings::values.vibration_strength.SetValue(ui->vibrationSpin->value()); + Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked()); } void QtControllerSelectorDialog::LoadConfiguration() { @@ -281,6 +283,8 @@ void QtControllerSelectorDialog::LoadConfiguration() { UpdateDockedState(Settings::values.players.GetValue()[8].connected); ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); + ui->vibrationSpin->setValue(Settings::values.vibration_strength.GetValue()); + ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); } void QtControllerSelectorDialog::CallConfigureInputDialog() { -- cgit v1.2.3 From d6a41cfc21a75349ca79e73da5ca1dcecd1af901 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 16 Oct 2020 11:55:45 -0400 Subject: settings: Remove global vibration strength modifier This will be replaced in favor of per-player vibration strength modifiers. --- src/yuzu/applets/controller.cpp | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 196b4f163..0fc713a6e 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -266,7 +266,6 @@ void QtControllerSelectorDialog::ApplyConfiguration() { OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked()); - Settings::values.vibration_strength.SetValue(ui->vibrationSpin->value()); Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked()); } @@ -283,7 +282,6 @@ void QtControllerSelectorDialog::LoadConfiguration() { UpdateDockedState(Settings::values.players.GetValue()[8].connected); ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); - ui->vibrationSpin->setValue(Settings::values.vibration_strength.GetValue()); ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); } -- cgit v1.2.3 From 38110dd485e329fa39e2e4c02b91a89dfebcbc88 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Sat, 17 Oct 2020 09:38:12 -0400 Subject: configure_input: Add per-player vibration Allows for enabling and modifying vibration and vibration strength per player. Also adds a toggle for enabling/disabling accurate vibrations. Co-authored-by: Its-Rei --- src/yuzu/applets/controller.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 0fc713a6e..c5e671309 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -14,6 +14,7 @@ #include "ui_controller.h" #include "yuzu/applets/controller.h" #include "yuzu/configuration/configure_input_dialog.h" +#include "yuzu/configuration/configure_vibration.h" #include "yuzu/main.h" namespace { @@ -223,6 +224,9 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( } } + connect(ui->vibrationButton, &QPushButton::clicked, this, + &QtControllerSelectorDialog::CallConfigureVibrationDialog); + connect(ui->inputConfigButton, &QPushButton::clicked, this, &QtControllerSelectorDialog::CallConfigureInputDialog); @@ -285,6 +289,18 @@ void QtControllerSelectorDialog::LoadConfiguration() { ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); } +void QtControllerSelectorDialog::CallConfigureVibrationDialog() { + ConfigureVibration dialog(this); + + dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint); + dialog.setWindowModality(Qt::WindowModal); + + if (dialog.exec() == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } +} + void QtControllerSelectorDialog::CallConfigureInputDialog() { const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; -- cgit v1.2.3 From e9e1876e821b8bd1bb5c8254ec93e2cc479e16dd Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 20 Oct 2020 13:55:25 -0400 Subject: input_common: Add VibrationDevice and VibrationDeviceFactory A vibration device is an input device that returns an unsigned byte as status. It represents whether the vibration device supports vibration or not. If the status returns 1, it supports vibration. Otherwise, it does not support vibration. --- src/yuzu/applets/controller.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index c5e671309..cdcd3d28d 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -478,6 +478,8 @@ void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) return; } + ConfigureVibration::SetVibrationDevices(player_index); + // Player 1 and Handheld auto& handheld = Settings::values.players.GetValue()[8]; // If Handheld is selected, copy all the settings from Player 1 to Handheld. -- cgit v1.2.3 From 91c06dae1a68efb2d6ae110d0aa28d7b3aafd573 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 23 Oct 2020 12:09:28 -0400 Subject: input: Disconnect a controller prior to connecting a new one Some games do not respond to a change in controller type if 1) The controller is not disconnected prior to being reconnected and/or 2) The controller is reconnected instantly after being disconnected. Since it is not possible to change controllers instantly on hardware and requiring a disconnect prior to connecting a new one, we should emulate this as well with a small delay, fixing the aforementioned issue. --- src/yuzu/applets/controller.cpp | 62 ++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 25 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index cdcd3d28d..552cb7204 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -19,6 +19,8 @@ namespace { +constexpr std::size_t HANDHELD_INDEX = 8; + constexpr std::array, 8> led_patterns{{ {true, false, false, false}, {true, true, false, false}, @@ -260,11 +262,6 @@ int QtControllerSelectorDialog::exec() { } void QtControllerSelectorDialog::ApplyConfiguration() { - // Update the controller state once more, just to be sure they are properly applied. - for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { - UpdateControllerState(index); - } - const bool pre_docked_mode = Settings::values.use_docked_mode.GetValue(); Settings::values.use_docked_mode.SetValue(ui->radioDocked->isChecked()); OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); @@ -275,15 +272,16 @@ void QtControllerSelectorDialog::ApplyConfiguration() { void QtControllerSelectorDialog::LoadConfiguration() { for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { - const auto connected = Settings::values.players.GetValue()[index].connected || - (index == 0 && Settings::values.players.GetValue()[8].connected); + const auto connected = + Settings::values.players.GetValue()[index].connected || + (index == 0 && Settings::values.players.GetValue()[HANDHELD_INDEX].connected); player_groupboxes[index]->setChecked(connected); connected_controller_checkboxes[index]->setChecked(connected); emulated_controllers[index]->setCurrentIndex( GetIndexFromControllerType(Settings::values.players.GetValue()[index].controller_type)); } - UpdateDockedState(Settings::values.players.GetValue()[8].connected); + UpdateDockedState(Settings::values.players.GetValue()[HANDHELD_INDEX].connected); ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); @@ -468,32 +466,46 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) { auto& player = Settings::values.players.GetValue()[player_index]; - player.controller_type = + const auto controller_type = GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()); - player.connected = player_groupboxes[player_index]->isChecked(); + const auto player_connected = player_groupboxes[player_index]->isChecked() && + controller_type != Settings::ControllerType::Handheld; - // Player 2-8 - if (player_index != 0) { - UpdateController(player.controller_type, player_index, player.connected); + if (player.controller_type == controller_type && player.connected == player_connected) { + // Set vibration devices in the event that the input device has changed. + ConfigureVibration::SetVibrationDevices(player_index); return; } + // Disconnect the controller first. + UpdateController(controller_type, player_index, false); + + player.controller_type = controller_type; + player.connected = player_connected; + ConfigureVibration::SetVibrationDevices(player_index); - // Player 1 and Handheld - auto& handheld = Settings::values.players.GetValue()[8]; - // If Handheld is selected, copy all the settings from Player 1 to Handheld. - if (player.controller_type == Settings::ControllerType::Handheld) { - handheld = player; - handheld.connected = player_groupboxes[player_index]->isChecked(); - player.connected = false; // Disconnect Player 1 - } else { - player.connected = player_groupboxes[player_index]->isChecked(); - handheld.connected = false; // Disconnect Handheld + // Handheld + if (player_index == 0) { + auto& handheld = Settings::values.players.GetValue()[HANDHELD_INDEX]; + if (controller_type == Settings::ControllerType::Handheld) { + handheld = player; + } + handheld.connected = player_groupboxes[player_index]->isChecked() && + controller_type == Settings::ControllerType::Handheld; + UpdateController(Settings::ControllerType::Handheld, 8, handheld.connected); + } + + if (!player.connected) { + return; } - UpdateController(player.controller_type, player_index, player.connected); - UpdateController(Settings::ControllerType::Handheld, 8, handheld.connected); + // This emulates a delay between disconnecting and reconnecting controllers as some games + // do not respond to a change in controller type if it was instantaneous. + using namespace std::chrono_literals; + std::this_thread::sleep_for(20ms); + + UpdateController(controller_type, player_index, player_connected); } void QtControllerSelectorDialog::UpdateLEDPattern(std::size_t player_index) { -- cgit v1.2.3 From 760a9e869322cbda51416f7001842557b90754af Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Mon, 26 Oct 2020 03:28:03 -0400 Subject: applets/controller: Change the input button to create input profiles Co-authored-by: Its-Rei --- src/yuzu/applets/controller.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 552cb7204..5112d48d2 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -13,8 +13,10 @@ #include "core/hle/service/sm/sm.h" #include "ui_controller.h" #include "yuzu/applets/controller.h" -#include "yuzu/configuration/configure_input_dialog.h" +#include "yuzu/configuration/configure_input.h" +#include "yuzu/configuration/configure_input_profile_dialog.h" #include "yuzu/configuration/configure_vibration.h" +#include "yuzu/configuration/input_profiles.h" #include "yuzu/main.h" namespace { @@ -109,7 +111,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( QWidget* parent, Core::Frontend::ControllerParameters parameters_, InputCommon::InputSubsystem* input_subsystem_) : QDialog(parent), ui(std::make_unique()), - parameters(std::move(parameters_)), input_subsystem(input_subsystem_) { + parameters(std::move(parameters_)), input_subsystem{input_subsystem_}, + input_profiles(std::make_unique()) { ui->setupUi(this); player_widgets = { @@ -230,7 +233,7 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( &QtControllerSelectorDialog::CallConfigureVibrationDialog); connect(ui->inputConfigButton, &QPushButton::clicked, this, - &QtControllerSelectorDialog::CallConfigureInputDialog); + &QtControllerSelectorDialog::CallConfigureInputProfileDialog); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QtControllerSelectorDialog::ApplyConfiguration); @@ -299,20 +302,13 @@ void QtControllerSelectorDialog::CallConfigureVibrationDialog() { } } -void QtControllerSelectorDialog::CallConfigureInputDialog() { - const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; - - ConfigureInputDialog dialog(this, max_supported_players, input_subsystem); +void QtControllerSelectorDialog::CallConfigureInputProfileDialog() { + ConfigureInputProfileDialog dialog(this, input_subsystem, input_profiles.get()); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint); dialog.setWindowModality(Qt::WindowModal); dialog.exec(); - - dialog.ApplyConfiguration(); - - LoadConfiguration(); - CheckIfParametersMet(); } bool QtControllerSelectorDialog::CheckIfParametersMet() { -- cgit v1.2.3 From 97b2220a822548eed83993fceebe0e611dbec84b Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 27 Oct 2020 13:33:25 -0400 Subject: general: Fix compiler warnings on linux and miscellaneous changes --- src/yuzu/applets/controller.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/yuzu/applets/controller.cpp') diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 5112d48d2..8ecfec770 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include "common/assert.h" #include "common/string_util.h" @@ -356,7 +357,7 @@ bool QtControllerSelectorDialog::CheckIfParametersMet() { } void QtControllerSelectorDialog::SetSupportedControllers() { - const QString theme = [this] { + const QString theme = [] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { @@ -445,7 +446,7 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) } }(); - const QString theme = [this] { + const QString theme = [] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { -- cgit v1.2.3