Przeglądaj źródła

input_profiles: Implement input profiles

Morph 5 lat temu
rodzic
commit
57d89e291d

+ 2 - 0
src/yuzu/CMakeLists.txt

@@ -108,6 +108,8 @@ add_executable(yuzu
     configuration/configure_web.cpp
     configuration/configure_web.h
     configuration/configure_web.ui
+    configuration/input_profiles.cpp
+    configuration/input_profiles.h
     debugger/console.cpp
     debugger/console.h
     debugger/profiler.cpp

+ 165 - 106
src/yuzu/configuration/config.cpp

@@ -5,6 +5,7 @@
 #include <array>
 #include <QKeySequence>
 #include <QSettings>
+#include "common/common_paths.h"
 #include "common/file_util.h"
 #include "core/hle/service/acc/profile_manager.h"
 #include "core/hle/service/hid/controllers/npad.h"
@@ -14,14 +15,27 @@
 
 namespace FS = Common::FS;
 
-Config::Config(const std::string& config_file, bool is_global) {
-    // TODO: Don't hardcode the path; let the frontend decide where to put the config files.
-    qt_config_loc = FS::GetUserPath(FS::UserPath::ConfigDir) + config_file;
-    FS::CreateFullPath(qt_config_loc);
-    qt_config =
-        std::make_unique<QSettings>(QString::fromStdString(qt_config_loc), QSettings::IniFormat);
-    global = is_global;
-    Reload();
+Config::Config(const std::string& config_file, ConfigType config_type) : type(config_type) {
+    global = config_type == ConfigType::GlobalConfig;
+
+    switch (config_type) {
+    case ConfigType::GlobalConfig:
+    case ConfigType::PerGameConfig:
+        qt_config_loc = fmt::format("{}" DIR_SEP "{}.ini", FS::GetUserPath(FS::UserPath::ConfigDir),
+                                    config_file);
+        FS::CreateFullPath(qt_config_loc);
+        qt_config = std::make_unique<QSettings>(QString::fromStdString(qt_config_loc),
+                                                QSettings::IniFormat);
+        Reload();
+        break;
+    case ConfigType::InputProfile:
+        qt_config_loc = fmt::format("{}input" DIR_SEP "{}.ini",
+                                    FS::GetUserPath(FS::UserPath::ConfigDir), config_file);
+        FS::CreateFullPath(qt_config_loc);
+        qt_config = std::make_unique<QSettings>(QString::fromStdString(qt_config_loc),
+                                                QSettings::IniFormat);
+        break;
+    }
 }
 
 Config::~Config() {
@@ -242,84 +256,103 @@ const std::array<UISettings::Shortcut, 16> Config::default_hotkeys{{
 }};
 // clang-format on
 
-void Config::ReadPlayerValues() {
-    for (std::size_t p = 0; p < Settings::values.players.size(); ++p) {
-        auto& player = Settings::values.players[p];
+void Config::ReadPlayerValue(std::size_t player_index) {
+    const QString player_prefix = [this, player_index] {
+        if (type == ConfigType::InputProfile) {
+            return QString{};
+        } else {
+            return QStringLiteral("player_%1_").arg(player_index);
+        }
+    }();
+
+    auto& player = Settings::values.players[player_index];
+
+    if (player_prefix.isEmpty()) {
+        const auto controller = static_cast<Settings::ControllerType>(
+            qt_config
+                ->value(QStringLiteral("%1type").arg(player_prefix),
+                        static_cast<u8>(Settings::ControllerType::ProController))
+                .toUInt());
 
+        if (controller == Settings::ControllerType::LeftJoycon ||
+            controller == Settings::ControllerType::RightJoycon) {
+            player.controller_type = controller;
+        }
+    } else {
         player.connected =
-            ReadSetting(QStringLiteral("player_%1_connected").arg(p), false).toBool();
+            ReadSetting(QStringLiteral("%1connected").arg(player_prefix), false).toBool();
 
         player.controller_type = static_cast<Settings::ControllerType>(
             qt_config
-                ->value(QStringLiteral("player_%1_type").arg(p),
+                ->value(QStringLiteral("%1type").arg(player_prefix),
                         static_cast<u8>(Settings::ControllerType::ProController))
                 .toUInt());
 
         player.body_color_left = qt_config
-                                     ->value(QStringLiteral("player_%1_body_color_left").arg(p),
+                                     ->value(QStringLiteral("%1body_color_left").arg(player_prefix),
                                              Settings::JOYCON_BODY_NEON_BLUE)
                                      .toUInt();
-        player.body_color_right = qt_config
-                                      ->value(QStringLiteral("player_%1_body_color_right").arg(p),
-                                              Settings::JOYCON_BODY_NEON_RED)
-                                      .toUInt();
-        player.button_color_left = qt_config
-                                       ->value(QStringLiteral("player_%1_button_color_left").arg(p),
-                                               Settings::JOYCON_BUTTONS_NEON_BLUE)
-                                       .toUInt();
+        player.body_color_right =
+            qt_config
+                ->value(QStringLiteral("%1body_color_right").arg(player_prefix),
+                        Settings::JOYCON_BODY_NEON_RED)
+                .toUInt();
+        player.button_color_left =
+            qt_config
+                ->value(QStringLiteral("%1button_color_left").arg(player_prefix),
+                        Settings::JOYCON_BUTTONS_NEON_BLUE)
+                .toUInt();
         player.button_color_right =
             qt_config
-                ->value(QStringLiteral("player_%1_button_color_right").arg(p),
+                ->value(QStringLiteral("%1button_color_right").arg(player_prefix),
                         Settings::JOYCON_BUTTONS_NEON_RED)
                 .toUInt();
+    }
 
-        for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
-            const std::string default_param =
-                InputCommon::GenerateKeyboardParam(default_buttons[i]);
-            auto& player_buttons = player.buttons[i];
-
-            player_buttons = qt_config
-                                 ->value(QStringLiteral("player_%1_").arg(p) +
-                                             QString::fromUtf8(Settings::NativeButton::mapping[i]),
-                                         QString::fromStdString(default_param))
-                                 .toString()
-                                 .toStdString();
-            if (player_buttons.empty()) {
-                player_buttons = default_param;
-            }
+    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
+        const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
+        auto& player_buttons = player.buttons[i];
+
+        player_buttons = qt_config
+                             ->value(QStringLiteral("%1").arg(player_prefix) +
+                                         QString::fromUtf8(Settings::NativeButton::mapping[i]),
+                                     QString::fromStdString(default_param))
+                             .toString()
+                             .toStdString();
+        if (player_buttons.empty()) {
+            player_buttons = default_param;
         }
+    }
 
-        for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
-            const std::string default_param =
-                InputCommon::GenerateKeyboardParam(default_motions[i]);
-            auto& player_motions = player.motions[i];
-
-            player_motions = qt_config
-                                 ->value(QStringLiteral("player_%1_").arg(p) +
-                                             QString::fromUtf8(Settings::NativeMotion::mapping[i]),
-                                         QString::fromStdString(default_param))
-                                 .toString()
-                                 .toStdString();
-            if (player_motions.empty()) {
-                player_motions = default_param;
-            }
+    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
+        const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
+        auto& player_motions = player.motions[i];
+
+        player_motions = qt_config
+                             ->value(QStringLiteral("%1").arg(player_prefix) +
+                                         QString::fromUtf8(Settings::NativeMotion::mapping[i]),
+                                     QString::fromStdString(default_param))
+                             .toString()
+                             .toStdString();
+        if (player_motions.empty()) {
+            player_motions = default_param;
         }
+    }
 
-        for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
-            const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
-                default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
-                default_analogs[i][3], default_stick_mod[i], 0.5f);
-            auto& player_analogs = player.analogs[i];
-
-            player_analogs = qt_config
-                                 ->value(QStringLiteral("player_%1_").arg(p) +
-                                             QString::fromUtf8(Settings::NativeAnalog::mapping[i]),
-                                         QString::fromStdString(default_param))
-                                 .toString()
-                                 .toStdString();
-            if (player_analogs.empty()) {
-                player_analogs = default_param;
-            }
+    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
+        const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
+            default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
+            default_analogs[i][3], default_stick_mod[i], 0.5f);
+        auto& player_analogs = player.analogs[i];
+
+        player_analogs = qt_config
+                             ->value(QStringLiteral("%1").arg(player_prefix) +
+                                         QString::fromUtf8(Settings::NativeAnalog::mapping[i]),
+                                     QString::fromStdString(default_param))
+                             .toString()
+                             .toStdString();
+        if (player_analogs.empty()) {
+            player_analogs = default_param;
         }
     }
 }
@@ -436,7 +469,9 @@ void Config::ReadAudioValues() {
 void Config::ReadControlValues() {
     qt_config->beginGroup(QStringLiteral("Controls"));
 
-    ReadPlayerValues();
+    for (std::size_t p = 0; p < Settings::values.players.size(); ++p) {
+        ReadPlayerValue(p);
+    }
     ReadDebugValues();
     ReadKeyboardValues();
     ReadMouseValues();
@@ -920,49 +955,55 @@ void Config::ReadValues() {
     ReadSystemValues();
 }
 
-void Config::SavePlayerValues() {
-    for (std::size_t p = 0; p < Settings::values.players.size(); ++p) {
-        const auto& player = Settings::values.players[p];
+void Config::SavePlayerValue(std::size_t player_index) {
+    const QString player_prefix = [this, player_index] {
+        if (type == ConfigType::InputProfile) {
+            return QString{};
+        } else {
+            return QStringLiteral("player_%1_").arg(player_index);
+        }
+    }();
+
+    const auto& player = Settings::values.players[player_index];
 
-        WriteSetting(QStringLiteral("player_%1_connected").arg(p), player.connected, false);
-        WriteSetting(QStringLiteral("player_%1_type").arg(p),
-                     static_cast<u8>(player.controller_type),
-                     static_cast<u8>(Settings::ControllerType::ProController));
+    WriteSetting(QStringLiteral("%1type").arg(player_prefix),
+                 static_cast<u8>(player.controller_type),
+                 static_cast<u8>(Settings::ControllerType::ProController));
 
-        WriteSetting(QStringLiteral("player_%1_body_color_left").arg(p), player.body_color_left,
+    if (!player_prefix.isEmpty()) {
+        WriteSetting(QStringLiteral("%1connected").arg(player_prefix), player.connected, false);
+        WriteSetting(QStringLiteral("%1body_color_left").arg(player_prefix), player.body_color_left,
                      Settings::JOYCON_BODY_NEON_BLUE);
-        WriteSetting(QStringLiteral("player_%1_body_color_right").arg(p), player.body_color_right,
-                     Settings::JOYCON_BODY_NEON_RED);
-        WriteSetting(QStringLiteral("player_%1_button_color_left").arg(p), player.button_color_left,
-                     Settings::JOYCON_BUTTONS_NEON_BLUE);
-        WriteSetting(QStringLiteral("player_%1_button_color_right").arg(p),
+        WriteSetting(QStringLiteral("%1body_color_right").arg(player_prefix),
+                     player.body_color_right, Settings::JOYCON_BODY_NEON_RED);
+        WriteSetting(QStringLiteral("%1button_color_left").arg(player_prefix),
+                     player.button_color_left, Settings::JOYCON_BUTTONS_NEON_BLUE);
+        WriteSetting(QStringLiteral("%1button_color_right").arg(player_prefix),
                      player.button_color_right, Settings::JOYCON_BUTTONS_NEON_RED);
+    }
 
-        for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
-            const std::string default_param =
-                InputCommon::GenerateKeyboardParam(default_buttons[i]);
-            WriteSetting(QStringLiteral("player_%1_").arg(p) +
-                             QString::fromStdString(Settings::NativeButton::mapping[i]),
-                         QString::fromStdString(player.buttons[i]),
-                         QString::fromStdString(default_param));
-        }
-        for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
-            const std::string default_param =
-                InputCommon::GenerateKeyboardParam(default_motions[i]);
-            WriteSetting(QStringLiteral("player_%1_").arg(p) +
-                             QString::fromStdString(Settings::NativeMotion::mapping[i]),
-                         QString::fromStdString(player.motions[i]),
-                         QString::fromStdString(default_param));
-        }
-        for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
-            const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
-                default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
-                default_analogs[i][3], default_stick_mod[i], 0.5f);
-            WriteSetting(QStringLiteral("player_%1_").arg(p) +
-                             QString::fromStdString(Settings::NativeAnalog::mapping[i]),
-                         QString::fromStdString(player.analogs[i]),
-                         QString::fromStdString(default_param));
-        }
+    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
+        const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
+        WriteSetting(QStringLiteral("%1").arg(player_prefix) +
+                         QString::fromStdString(Settings::NativeButton::mapping[i]),
+                     QString::fromStdString(player.buttons[i]),
+                     QString::fromStdString(default_param));
+    }
+    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
+        const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
+        WriteSetting(QStringLiteral("%1").arg(player_prefix) +
+                         QString::fromStdString(Settings::NativeMotion::mapping[i]),
+                     QString::fromStdString(player.motions[i]),
+                     QString::fromStdString(default_param));
+    }
+    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
+        const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
+            default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
+            default_analogs[i][3], default_stick_mod[i], 0.5f);
+        WriteSetting(QStringLiteral("%1").arg(player_prefix) +
+                         QString::fromStdString(Settings::NativeAnalog::mapping[i]),
+                     QString::fromStdString(player.analogs[i]),
+                     QString::fromStdString(default_param));
     }
 }
 
@@ -1087,7 +1128,9 @@ void Config::SaveAudioValues() {
 void Config::SaveControlValues() {
     qt_config->beginGroup(QStringLiteral("Controls"));
 
-    SavePlayerValues();
+    for (std::size_t p = 0; p < Settings::values.players.size(); ++p) {
+        SavePlayerValue(p);
+    }
     SaveDebugValues();
     SaveMouseValues();
     SaveTouchscreenValues();
@@ -1515,3 +1558,19 @@ void Config::Save() {
     Settings::Sanitize();
     SaveValues();
 }
+
+void Config::ReadControlPlayerValue(std::size_t player_index) {
+    qt_config->beginGroup(QStringLiteral("Controls"));
+    ReadPlayerValue(player_index);
+    qt_config->endGroup();
+}
+
+void Config::SaveControlPlayerValue(std::size_t player_index) {
+    qt_config->beginGroup(QStringLiteral("Controls"));
+    SavePlayerValue(player_index);
+    qt_config->endGroup();
+}
+
+const std::string& Config::GetConfigFilePath() const {
+    return qt_config_loc;
+}

+ 16 - 4
src/yuzu/configuration/config.h

@@ -16,12 +16,24 @@ class QSettings;
 
 class Config {
 public:
-    explicit Config(const std::string& config_loc = "qt-config.ini", bool is_global = true);
+    enum class ConfigType {
+        GlobalConfig,
+        PerGameConfig,
+        InputProfile,
+    };
+
+    explicit Config(const std::string& config_loc = "qt-config",
+                    ConfigType config_type = ConfigType::GlobalConfig);
     ~Config();
 
     void Reload();
     void Save();
 
+    void ReadControlPlayerValue(std::size_t player_index);
+    void SaveControlPlayerValue(std::size_t player_index);
+
+    const std::string& GetConfigFilePath() const;
+
     static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
     static const std::array<int, Settings::NativeMotion::NumMotions> default_motions;
     static const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> default_analogs;
@@ -34,7 +46,7 @@ public:
 
 private:
     void ReadValues();
-    void ReadPlayerValues();
+    void ReadPlayerValue(std::size_t player_index);
     void ReadDebugValues();
     void ReadKeyboardValues();
     void ReadMouseValues();
@@ -62,7 +74,7 @@ private:
     void ReadWebServiceValues();
 
     void SaveValues();
-    void SavePlayerValues();
+    void SavePlayerValue(std::size_t player_index);
     void SaveDebugValues();
     void SaveMouseValues();
     void SaveTouchscreenValues();
@@ -111,9 +123,9 @@ private:
     void WriteSettingGlobal(const QString& name, const QVariant& value, bool use_global,
                             const QVariant& default_value);
 
+    ConfigType type;
     std::unique_ptr<QSettings> qt_config;
     std::string qt_config_loc;
-
     bool global;
 };
 

+ 4 - 2
src/yuzu/configuration/configure_debug_controller.cpp

@@ -6,9 +6,11 @@
 #include "yuzu/configuration/configure_debug_controller.h"
 
 ConfigureDebugController::ConfigureDebugController(QWidget* parent,
-                                                   InputCommon::InputSubsystem* input_subsystem)
+                                                   InputCommon::InputSubsystem* input_subsystem,
+                                                   InputProfiles* profiles)
     : QDialog(parent), ui(std::make_unique<Ui::ConfigureDebugController>()),
-      debug_controller(new ConfigureInputPlayer(this, 9, nullptr, input_subsystem, true)) {
+      debug_controller(
+          new ConfigureInputPlayer(this, 9, nullptr, input_subsystem, profiles, true)) {
     ui->setupUi(this);
 
     ui->controllerLayout->addWidget(debug_controller);

+ 4 - 2
src/yuzu/configuration/configure_debug_controller.h

@@ -10,6 +10,8 @@
 
 class QPushButton;
 
+class InputProfiles;
+
 namespace InputCommon {
 class InputSubsystem;
 }
@@ -22,8 +24,8 @@ class ConfigureDebugController : public QDialog {
     Q_OBJECT
 
 public:
-    explicit ConfigureDebugController(QWidget* parent,
-                                      InputCommon::InputSubsystem* input_subsystem);
+    explicit ConfigureDebugController(QWidget* parent, InputCommon::InputSubsystem* input_subsystem,
+                                      InputProfiles* profiles);
     ~ConfigureDebugController() override;
 
     void ApplyConfiguration();

+ 20 - 10
src/yuzu/configuration/configure_input.cpp

@@ -23,6 +23,7 @@
 #include "yuzu/configuration/configure_motion_touch.h"
 #include "yuzu/configuration/configure_mouse_advanced.h"
 #include "yuzu/configuration/configure_touchscreen_advanced.h"
+#include "yuzu/configuration/input_profiles.h"
 
 namespace {
 template <typename Dialog, typename... Args>
@@ -64,7 +65,8 @@ void OnDockedModeChanged(bool last_state, bool new_state) {
 }
 
 ConfigureInput::ConfigureInput(QWidget* parent)
-    : QWidget(parent), ui(std::make_unique<Ui::ConfigureInput>()) {
+    : QWidget(parent), ui(std::make_unique<Ui::ConfigureInput>()),
+      profiles(std::make_unique<InputProfiles>()) {
     ui->setupUi(this);
 }
 
@@ -73,14 +75,22 @@ ConfigureInput::~ConfigureInput() = default;
 void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
                                 std::size_t max_players) {
     player_controllers = {
-        new ConfigureInputPlayer(this, 0, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 1, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 2, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 3, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 4, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 5, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 6, ui->consoleInputSettings, input_subsystem),
-        new ConfigureInputPlayer(this, 7, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 0, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 1, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 2, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 3, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 4, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 5, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 6, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
+        new ConfigureInputPlayer(this, 7, ui->consoleInputSettings, input_subsystem,
+                                 profiles.get()),
     };
 
     player_tabs = {
@@ -134,7 +144,7 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
     ui->tabAdvanced->setLayout(new QHBoxLayout(ui->tabAdvanced));
     ui->tabAdvanced->layout()->addWidget(advanced);
     connect(advanced, &ConfigureInputAdvanced::CallDebugControllerDialog, [this, input_subsystem] {
-        CallConfigureDialog<ConfigureDebugController>(*this, input_subsystem);
+        CallConfigureDialog<ConfigureDebugController>(*this, input_subsystem, profiles.get());
     });
     connect(advanced, &ConfigureInputAdvanced::CallMouseConfigDialog, [this, input_subsystem] {
         CallConfigureDialog<ConfigureMouseAdvanced>(*this, input_subsystem);

+ 4 - 0
src/yuzu/configuration/configure_input.h

@@ -19,6 +19,8 @@ class QCheckBox;
 class QString;
 class QTimer;
 
+class InputProfiles;
+
 namespace InputCommon {
 class InputSubsystem;
 }
@@ -61,6 +63,8 @@ private:
 
     std::unique_ptr<Ui::ConfigureInput> ui;
 
+    std::unique_ptr<InputProfiles> profiles;
+
     std::array<ConfigureInputPlayer*, 8> player_controllers;
     std::array<QWidget*, 8> player_tabs;
     std::array<QCheckBox*, 8> player_connected;

+ 108 - 3
src/yuzu/configuration/configure_input_player.cpp

@@ -22,6 +22,8 @@
 #include "ui_configure_input_player.h"
 #include "yuzu/configuration/config.h"
 #include "yuzu/configuration/configure_input_player.h"
+#include "yuzu/configuration/input_profiles.h"
+#include "yuzu/util/limitable_input_dialog.h"
 
 constexpr std::size_t HANDHELD_INDEX = 8;
 
@@ -240,10 +242,11 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir)
 ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_index,
                                            QWidget* bottom_row,
                                            InputCommon::InputSubsystem* input_subsystem_,
-                                           bool debug)
+                                           InputProfiles* profiles_, bool debug)
     : QWidget(parent), ui(std::make_unique<Ui::ConfigureInputPlayer>()), player_index(player_index),
-      debug(debug), input_subsystem{input_subsystem_}, timeout_timer(std::make_unique<QTimer>()),
-      poll_timer(std::make_unique<QTimer>()), bottom_row(bottom_row) {
+      debug(debug), input_subsystem{input_subsystem_}, profiles(profiles_),
+      timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()),
+      bottom_row(bottom_row) {
     ui->setupUi(this);
 
     setFocusPolicy(Qt::ClickFocus);
@@ -521,6 +524,17 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
         }
     });
 
+    RefreshInputProfiles();
+
+    connect(ui->buttonProfilesNew, &QPushButton::clicked, this,
+            &ConfigureInputPlayer::CreateProfile);
+    connect(ui->buttonProfilesDelete, &QPushButton::clicked, this,
+            &ConfigureInputPlayer::DeleteProfile);
+    connect(ui->comboProfiles, qOverload<int>(&QComboBox::activated), this,
+            &ConfigureInputPlayer::LoadProfile);
+    connect(ui->buttonProfilesSave, &QPushButton::clicked, this,
+            &ConfigureInputPlayer::SaveProfile);
+
     LoadConfiguration();
 
     // TODO(wwylele): enable this when we actually emulate it
@@ -1061,3 +1075,94 @@ void ConfigureInputPlayer::keyPressEvent(QKeyEvent* event) {
 
     SetPollingResult({}, true);
 }
+
+void ConfigureInputPlayer::CreateProfile() {
+    const auto profile_name =
+        LimitableInputDialog::GetText(this, tr("New Profile"), tr("Enter a profile name:"), 1, 20);
+
+    if (profile_name.isEmpty()) {
+        return;
+    }
+
+    if (!profiles->IsProfileNameValid(profile_name.toStdString())) {
+        QMessageBox::critical(this, tr("Create Input Profile"),
+                              tr("The given profile name is not valid!"));
+        return;
+    }
+
+    ApplyConfiguration();
+
+    if (!profiles->CreateProfile(profile_name.toStdString(), player_index)) {
+        QMessageBox::critical(this, tr("Create Input Profile"),
+                              tr("Failed to create the input profile \"%1\"").arg(profile_name));
+        RefreshInputProfiles();
+        return;
+    }
+
+    ui->comboProfiles->addItem(profile_name);
+    ui->comboProfiles->setCurrentIndex(ui->comboProfiles->count() - 1);
+}
+
+void ConfigureInputPlayer::DeleteProfile() {
+    const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
+
+    if (profile_name.isEmpty()) {
+        return;
+    }
+
+    if (!profiles->DeleteProfile(profile_name.toStdString())) {
+        QMessageBox::critical(this, tr("Delete Input Profile"),
+                              tr("Failed to delete the input profile \"%1\"").arg(profile_name));
+        RefreshInputProfiles();
+        return;
+    }
+
+    ui->comboProfiles->removeItem(ui->comboProfiles->currentIndex());
+    ui->comboProfiles->setCurrentIndex(-1);
+}
+
+void ConfigureInputPlayer::LoadProfile() {
+    const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
+
+    if (profile_name.isEmpty()) {
+        return;
+    }
+
+    ApplyConfiguration();
+
+    if (!profiles->LoadProfile(profile_name.toStdString(), player_index)) {
+        QMessageBox::critical(this, tr("Load Input Profile"),
+                              tr("Failed to load the input profile \"%1\"").arg(profile_name));
+        RefreshInputProfiles();
+        return;
+    }
+
+    LoadConfiguration();
+}
+
+void ConfigureInputPlayer::SaveProfile() {
+    const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
+
+    if (profile_name.isEmpty()) {
+        return;
+    }
+
+    ApplyConfiguration();
+
+    if (!profiles->SaveProfile(profile_name.toStdString(), player_index)) {
+        QMessageBox::critical(this, tr("Save Input Profile"),
+                              tr("Failed to save the input profile \"%1\"").arg(profile_name));
+        RefreshInputProfiles();
+        return;
+    }
+}
+
+void ConfigureInputPlayer::RefreshInputProfiles() {
+    ui->comboProfiles->clear();
+
+    for (const auto& profile_name : profiles->GetInputProfileNames()) {
+        ui->comboProfiles->addItem(QString::fromStdString(profile_name));
+    }
+
+    ui->comboProfiles->setCurrentIndex(-1);
+}

+ 20 - 1
src/yuzu/configuration/configure_input_player.h

@@ -26,6 +26,8 @@ class QString;
 class QTimer;
 class QWidget;
 
+class InputProfiles;
+
 namespace InputCommon {
 class InputSubsystem;
 }
@@ -45,7 +47,7 @@ class ConfigureInputPlayer : public QWidget {
 public:
     explicit ConfigureInputPlayer(QWidget* parent, std::size_t player_index, QWidget* bottom_row,
                                   InputCommon::InputSubsystem* input_subsystem_,
-                                  bool debug = false);
+                                  InputProfiles* profiles_, bool debug = false);
     ~ConfigureInputPlayer() override;
 
     /// Save all button configurations to settings file.
@@ -116,6 +118,21 @@ private:
     /// Gets the default controller mapping for this device and auto configures the input to match.
     void UpdateMappingWithDefaults();
 
+    /// Creates a controller profile.
+    void CreateProfile();
+
+    /// Deletes the selected controller profile.
+    void DeleteProfile();
+
+    /// Loads the selected controller profile.
+    void LoadProfile();
+
+    /// Saves the current controller configuration into a selected controller profile.
+    void SaveProfile();
+
+    /// Refreshes the list of controller profiles.
+    void RefreshInputProfiles();
+
     std::unique_ptr<Ui::ConfigureInputPlayer> ui;
 
     std::size_t player_index;
@@ -123,6 +140,8 @@ private:
 
     InputCommon::InputSubsystem* input_subsystem;
 
+    InputProfiles* profiles;
+
     std::unique_ptr<QTimer> timeout_timer;
     std::unique_ptr<QTimer> poll_timer;
 

+ 2 - 1
src/yuzu/configuration/configure_per_game.cpp

@@ -29,7 +29,8 @@
 
 ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id)
     : QDialog(parent), ui(std::make_unique<Ui::ConfigurePerGame>()), title_id(title_id) {
-    game_config = std::make_unique<Config>(fmt::format("{:016X}.ini", title_id), false);
+    game_config = std::make_unique<Config>(fmt::format("{:016X}", title_id),
+                                           Config::ConfigType::PerGameConfig);
 
     Settings::SetConfiguringGlobal(false);
 

+ 131 - 0
src/yuzu/configuration/input_profiles.cpp

@@ -0,0 +1,131 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <fmt/format.h>
+
+#include "common/common_paths.h"
+#include "common/file_util.h"
+#include "yuzu/configuration/config.h"
+#include "yuzu/configuration/input_profiles.h"
+
+namespace FS = Common::FS;
+
+namespace {
+
+bool ProfileExistsInFilesystem(std::string_view profile_name) {
+    return FS::Exists(fmt::format("{}input" DIR_SEP "{}.ini",
+                                  FS::GetUserPath(FS::UserPath::ConfigDir), profile_name));
+}
+
+bool IsINI(std::string_view filename) {
+    const std::size_t index = filename.rfind('.');
+
+    if (index == std::string::npos) {
+        return false;
+    }
+
+    return filename.substr(index) == ".ini";
+}
+
+std::string GetNameWithoutExtension(const std::string& filename) {
+    const std::size_t index = filename.rfind('.');
+
+    if (index == std::string::npos) {
+        return filename;
+    }
+
+    return filename.substr(0, index);
+}
+
+} // namespace
+
+InputProfiles::InputProfiles() {
+    const std::string input_profile_loc =
+        fmt::format("{}input", FS::GetUserPath(FS::UserPath::ConfigDir));
+
+    FS::ForeachDirectoryEntry(
+        nullptr, input_profile_loc,
+        [this](u64* entries_out, const std::string& directory, const std::string& filename) {
+            if (IsINI(filename) && IsProfileNameValid(GetNameWithoutExtension(filename))) {
+                map_profiles.insert_or_assign(
+                    GetNameWithoutExtension(filename),
+                    std::make_unique<Config>(GetNameWithoutExtension(filename),
+                                             Config::ConfigType::InputProfile));
+            }
+            return true;
+        });
+}
+
+InputProfiles::~InputProfiles() = default;
+
+std::vector<std::string> InputProfiles::GetInputProfileNames() {
+    std::vector<std::string> profile_names;
+    profile_names.reserve(map_profiles.size());
+
+    for (const auto& [profile_name, config] : map_profiles) {
+        if (!ProfileExistsInFilesystem(profile_name)) {
+            DeleteProfile(profile_name);
+            continue;
+        }
+
+        profile_names.push_back(profile_name);
+    }
+
+    return profile_names;
+}
+
+bool InputProfiles::IsProfileNameValid(std::string_view profile_name) {
+    return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
+}
+
+bool InputProfiles::CreateProfile(const std::string& profile_name, std::size_t player_index) {
+    if (ProfileExistsInMap(profile_name)) {
+        return false;
+    }
+
+    map_profiles.insert_or_assign(
+        profile_name, std::make_unique<Config>(profile_name, Config::ConfigType::InputProfile));
+
+    return SaveProfile(profile_name, player_index);
+}
+
+bool InputProfiles::DeleteProfile(const std::string& profile_name) {
+    if (!ProfileExistsInMap(profile_name)) {
+        return false;
+    }
+
+    if (!ProfileExistsInFilesystem(profile_name) ||
+        FS::Delete(map_profiles[profile_name]->GetConfigFilePath())) {
+        map_profiles.erase(profile_name);
+    }
+
+    return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
+}
+
+bool InputProfiles::LoadProfile(const std::string& profile_name, std::size_t player_index) {
+    if (!ProfileExistsInMap(profile_name)) {
+        return false;
+    }
+
+    if (!ProfileExistsInFilesystem(profile_name)) {
+        map_profiles.erase(profile_name);
+        return false;
+    }
+
+    map_profiles[profile_name]->ReadControlPlayerValue(player_index);
+    return true;
+}
+
+bool InputProfiles::SaveProfile(const std::string& profile_name, std::size_t player_index) {
+    if (!ProfileExistsInMap(profile_name)) {
+        return false;
+    }
+
+    map_profiles[profile_name]->SaveControlPlayerValue(player_index);
+    return true;
+}
+
+bool InputProfiles::ProfileExistsInMap(const std::string& profile_name) const {
+    return map_profiles.find(profile_name) != map_profiles.end();
+}

+ 32 - 0
src/yuzu/configuration/input_profiles.h

@@ -0,0 +1,32 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+class Config;
+
+class InputProfiles {
+
+public:
+    explicit InputProfiles();
+    virtual ~InputProfiles();
+
+    std::vector<std::string> GetInputProfileNames();
+
+    static bool IsProfileNameValid(std::string_view profile_name);
+
+    bool CreateProfile(const std::string& profile_name, std::size_t player_index);
+    bool DeleteProfile(const std::string& profile_name);
+    bool LoadProfile(const std::string& profile_name, std::size_t player_index);
+    bool SaveProfile(const std::string& profile_name, std::size_t player_index);
+
+private:
+    bool ProfileExistsInMap(const std::string& profile_name) const;
+
+    std::unordered_map<std::string, std::unique_ptr<Config>> map_profiles;
+};

+ 1 - 1
src/yuzu/main.cpp

@@ -1087,7 +1087,7 @@ void GMainWindow::BootGame(const QString& filename) {
     const auto loader = Loader::GetLoader(v_file);
     if (!(loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success)) {
         // Load per game settings
-        Config per_game_config(fmt::format("{:016X}.ini", title_id), false);
+        Config per_game_config(fmt::format("{:016X}", title_id), Config::ConfigType::PerGameConfig);
     }
 
     Settings::LogSettings();