summaryrefslogtreecommitdiff
path: root/src/yuzu
diff options
context:
space:
mode:
Diffstat (limited to 'src/yuzu')
-rw-r--r--src/yuzu/CMakeLists.txt4
-rw-r--r--src/yuzu/applets/qt_amiibo_settings.cpp3
-rw-r--r--src/yuzu/applets/qt_controller.cpp53
-rw-r--r--src/yuzu/applets/qt_controller.h6
-rw-r--r--src/yuzu/applets/qt_controller.ui54
-rw-r--r--src/yuzu/bootmanager.cpp4
-rw-r--r--src/yuzu/configuration/config.cpp13
-rw-r--r--src/yuzu/configuration/config.h3
-rw-r--r--src/yuzu/configuration/configure_audio.cpp3
-rw-r--r--src/yuzu/configuration/configure_debug.cpp3
-rw-r--r--src/yuzu/configuration/configure_debug.ui99
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp28
-rw-r--r--src/yuzu/configuration/configure_input.cpp50
-rw-r--r--src/yuzu/configuration/configure_input.h1
-rw-r--r--src/yuzu/configuration/configure_per_game.cpp7
-rw-r--r--src/yuzu/configuration/configure_system.cpp5
-rw-r--r--src/yuzu/configuration/configure_ui.cpp12
-rw-r--r--src/yuzu/configuration/configure_ui.ui7
-rw-r--r--src/yuzu/configuration/shared_translation.cpp16
-rw-r--r--src/yuzu/configuration/shared_widget.cpp234
-rw-r--r--src/yuzu/configuration/shared_widget.h27
-rw-r--r--src/yuzu/game_list.cpp32
-rw-r--r--src/yuzu/game_list.h11
-rw-r--r--src/yuzu/game_list_p.h26
-rw-r--r--src/yuzu/game_list_worker.cpp39
-rw-r--r--src/yuzu/game_list_worker.h11
-rw-r--r--src/yuzu/hotkeys.h4
-rw-r--r--src/yuzu/main.cpp847
-rw-r--r--src/yuzu/main.h52
-rw-r--r--src/yuzu/main.ui49
-rw-r--r--src/yuzu/play_time_manager.cpp179
-rw-r--r--src/yuzu/play_time_manager.h44
-rw-r--r--src/yuzu/uisettings.h16
-rw-r--r--src/yuzu/util/util.cpp102
-rw-r--r--src/yuzu/util/util.h14
35 files changed, 1701 insertions, 357 deletions
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 8f86a1553..34208ed74 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -195,6 +195,8 @@ add_executable(yuzu
multiplayer/state.cpp
multiplayer/state.h
multiplayer/validation.h
+ play_time_manager.cpp
+ play_time_manager.h
precompiled_headers.h
qt_common.cpp
qt_common.h
@@ -382,7 +384,7 @@ if (USE_DISCORD_PRESENCE)
discord_impl.cpp
discord_impl.h
)
- target_link_libraries(yuzu PRIVATE DiscordRPC::discord-rpc httplib::httplib)
+ target_link_libraries(yuzu PRIVATE DiscordRPC::discord-rpc httplib::httplib Qt${QT_MAJOR_VERSION}::Network)
target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE)
endif()
diff --git a/src/yuzu/applets/qt_amiibo_settings.cpp b/src/yuzu/applets/qt_amiibo_settings.cpp
index 4988fcc83..b457a736a 100644
--- a/src/yuzu/applets/qt_amiibo_settings.cpp
+++ b/src/yuzu/applets/qt_amiibo_settings.cpp
@@ -160,7 +160,8 @@ void QtAmiiboSettingsDialog::LoadAmiiboData() {
}
const auto amiibo_name = std::string(register_info.amiibo_name.data());
- const auto owner_name = Common::UTF16ToUTF8(register_info.mii_char_info.name.data());
+ const auto owner_name =
+ Common::UTF16ToUTF8(register_info.mii_char_info.GetNickname().data.data());
const auto creation_date =
QDate(register_info.creation_date.year, register_info.creation_date.month,
register_info.creation_date.day);
diff --git a/src/yuzu/applets/qt_controller.cpp b/src/yuzu/applets/qt_controller.cpp
index 00aafb8f8..ca0e14fad 100644
--- a/src/yuzu/applets/qt_controller.cpp
+++ b/src/yuzu/applets/qt_controller.cpp
@@ -5,6 +5,8 @@
#include <thread>
#include "common/assert.h"
+#include "common/settings.h"
+#include "common/settings_enums.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/hid/emulated_controller.h"
@@ -21,6 +23,7 @@
#include "yuzu/configuration/configure_vibration.h"
#include "yuzu/configuration/input_profiles.h"
#include "yuzu/main.h"
+#include "yuzu/util/controller_navigation.h"
namespace {
@@ -130,6 +133,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
ui->checkboxPlayer7Connected, ui->checkboxPlayer8Connected,
};
+ ui->labelError->setVisible(false);
+
// Setup/load everything prior to setting up connections.
// This avoids unintentionally changing the states of elements while loading them in.
SetSupportedControllers();
@@ -141,6 +146,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
LoadConfiguration();
+ controller_navigation = new ControllerNavigation(system.HIDCore(), this);
+
for (std::size_t i = 0; i < NUM_PLAYERS; ++i) {
SetExplainText(i);
UpdateControllerIcon(i);
@@ -149,6 +156,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
connect(player_groupboxes[i], &QGroupBox::toggled, [this, i](bool checked) {
if (checked) {
+ // Hide eventual error message about number of controllers
+ ui->labelError->setVisible(false);
for (std::size_t index = 0; index <= i; ++index) {
connected_controller_checkboxes[index]->setChecked(checked);
}
@@ -197,6 +206,12 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
&QtControllerSelectorDialog::ApplyConfiguration);
+ connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
+ [this](Qt::Key key) {
+ QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ QCoreApplication::postEvent(this, event);
+ });
+
// 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.
@@ -215,6 +230,7 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
}
QtControllerSelectorDialog::~QtControllerSelectorDialog() {
+ controller_navigation->UnloadController();
system.HIDCore().DisableAllControllerConfiguration();
}
@@ -226,9 +242,11 @@ int QtControllerSelectorDialog::exec() {
}
void QtControllerSelectorDialog::ApplyConfiguration() {
- 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(), system);
+ const bool pre_docked_mode = Settings::IsDockedMode();
+ const bool docked_mode_selected = ui->radioDocked->isChecked();
+ Settings::values.use_docked_mode.SetValue(
+ docked_mode_selected ? Settings::ConsoleMode::Docked : Settings::ConsoleMode::Handheld);
+ OnDockedModeChanged(pre_docked_mode, docked_mode_selected, system);
Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked());
Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked());
@@ -287,6 +305,31 @@ void QtControllerSelectorDialog::CallConfigureInputProfileDialog() {
dialog.exec();
}
+void QtControllerSelectorDialog::keyPressEvent(QKeyEvent* evt) {
+ const auto num_connected_players = static_cast<int>(
+ std::count_if(player_groupboxes.begin(), player_groupboxes.end(),
+ [](const QGroupBox* player) { return player->isChecked(); }));
+
+ const auto min_supported_players = parameters.enable_single_mode ? 1 : parameters.min_players;
+ const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players;
+
+ if ((evt->key() == Qt::Key_Enter || evt->key() == Qt::Key_Return) && !parameters_met) {
+ // Display error message when trying to validate using "Enter" and "OK" button is disabled
+ ui->labelError->setVisible(true);
+ return;
+ } else if (evt->key() == Qt::Key_Left && num_connected_players > min_supported_players) {
+ // Remove a player if possible
+ connected_controller_checkboxes[num_connected_players - 1]->setChecked(false);
+ return;
+ } else if (evt->key() == Qt::Key_Right && num_connected_players < max_supported_players) {
+ // Add a player, if possible
+ ui->labelError->setVisible(false);
+ connected_controller_checkboxes[num_connected_players]->setChecked(true);
+ return;
+ }
+ QDialog::keyPressEvent(evt);
+}
+
bool QtControllerSelectorDialog::CheckIfParametersMet() {
// Here, we check and validate the current configuration against all applicable parameters.
const auto num_connected_players = static_cast<int>(
@@ -616,8 +659,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.GetValue());
- ui->radioUndocked->setChecked(!Settings::values.use_docked_mode.GetValue());
+ ui->radioDocked->setChecked(Settings::IsDockedMode());
+ ui->radioUndocked->setChecked(!Settings::IsDockedMode());
// Also force into undocked mode if the controller type is handheld.
if (is_handheld) {
diff --git a/src/yuzu/applets/qt_controller.h b/src/yuzu/applets/qt_controller.h
index 2fdc35857..7f0673d06 100644
--- a/src/yuzu/applets/qt_controller.h
+++ b/src/yuzu/applets/qt_controller.h
@@ -34,6 +34,8 @@ class HIDCore;
enum class NpadStyleIndex : u8;
} // namespace Core::HID
+class ControllerNavigation;
+
class QtControllerSelectorDialog final : public QDialog {
Q_OBJECT
@@ -46,6 +48,8 @@ public:
int exec() override;
+ void keyPressEvent(QKeyEvent* evt) override;
+
private:
// Applies the current configuration.
void ApplyConfiguration();
@@ -110,6 +114,8 @@ private:
Core::System& system;
+ ControllerNavigation* controller_navigation = nullptr;
+
// This is true if and only if all parameters are met. Otherwise, this is false.
// This determines whether the "OK" button can be clicked to exit the applet.
bool parameters_met{false};
diff --git a/src/yuzu/applets/qt_controller.ui b/src/yuzu/applets/qt_controller.ui
index 729e921ee..6f7cb3c13 100644
--- a/src/yuzu/applets/qt_controller.ui
+++ b/src/yuzu/applets/qt_controller.ui
@@ -2624,13 +2624,53 @@
</spacer>
</item>
<item alignment="Qt::AlignBottom">
- <widget class="QDialogButtonBox" name="buttonBox">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
- </property>
+ <widget class="QWidget" name="closeButtons" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_46">
+ <property name="spacing">
+ <number>7</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="labelError">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel { color : red; }</string>
+ </property>
+ <property name="text">
+ <string>Not enough controllers</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index 407988b8f..2afa72140 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -928,8 +928,8 @@ void GRenderWindow::CaptureScreenshot(const QString& screenshot_path) {
const Layout::FramebufferLayout layout{[]() {
u32 height = UISettings::values.screenshot_height.GetValue();
if (height == 0) {
- height = Settings::values.use_docked_mode.GetValue() ? Layout::ScreenDocked::Height
- : Layout::ScreenUndocked::Height;
+ height = Settings::IsDockedMode() ? Layout::ScreenDocked::Height
+ : Layout::ScreenUndocked::Height;
height *= Settings::values.resolution_info.up_factor;
}
const u32 width =
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index b22c83303..d5157c502 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -9,6 +9,7 @@
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "common/settings_common.h"
+#include "common/settings_enums.h"
#include "core/core.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/hid/controllers/npad.h"
@@ -85,9 +86,9 @@ const std::map<Settings::ScalingFilter, QString> Config::scaling_filter_texts_ma
{Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))},
};
-const std::map<bool, QString> Config::use_docked_mode_texts_map = {
- {true, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Docked"))},
- {false, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Handheld"))},
+const std::map<Settings::ConsoleMode, QString> Config::use_docked_mode_texts_map = {
+ {Settings::ConsoleMode::Docked, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Docked"))},
+ {Settings::ConsoleMode::Handheld, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Handheld"))},
};
const std::map<Settings::GpuAccuracy, QString> Config::gpu_accuracy_texts_map = {
@@ -127,8 +128,8 @@ const std::array<UISettings::Shortcut, 22> Config::default_hotkeys{{
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F11"), QStringLiteral("Home+B"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+O"), QStringLiteral(""), Qt::WidgetWithChildrenShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F2"), QStringLiteral("Home+A"), Qt::WidgetWithChildrenShortcut, false}},
- {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F6"), QStringLiteral(""), Qt::WindowShortcut, false}},
- {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F5"), QStringLiteral(""), Qt::WindowShortcut, false}},
+ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F6"), QStringLiteral("R+Plus+Minus"), Qt::WindowShortcut, false}},
+ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F5"), QStringLiteral("L+Plus+Minus"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Record")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F7"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F6"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F5"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
@@ -376,7 +377,7 @@ void Config::ReadControlValues() {
const auto controller_type = Settings::values.players.GetValue()[0].controller_type;
if (controller_type == Settings::ControllerType::Handheld) {
Settings::values.use_docked_mode.SetGlobal(!IsCustomConfig());
- Settings::values.use_docked_mode.SetValue(false);
+ Settings::values.use_docked_mode.SetValue(Settings::ConsoleMode::Handheld);
}
if (IsCustomConfig()) {
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index 0ac74c8e7..727feebfb 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -9,6 +9,7 @@
#include <QMetaType>
#include <QVariant>
#include "common/settings.h"
+#include "common/settings_enums.h"
#include "yuzu/uisettings.h"
class QSettings;
@@ -51,7 +52,7 @@ public:
static const std::map<Settings::AntiAliasing, QString> anti_aliasing_texts_map;
static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map;
- static const std::map<bool, QString> use_docked_mode_texts_map;
+ static const std::map<Settings::ConsoleMode, QString> use_docked_mode_texts_map;
static const std::map<Settings::GpuAccuracy, QString> gpu_accuracy_texts_map;
static const std::map<Settings::RendererBackend, QString> renderer_backend_texts_map;
static const std::map<Settings::ShaderBackend, QString> shader_backend_texts_map;
diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp
index 9ccfb2435..81dd51ad3 100644
--- a/src/yuzu/configuration/configure_audio.cpp
+++ b/src/yuzu/configuration/configure_audio.cpp
@@ -42,6 +42,9 @@ void ConfigureAudio::Setup(const ConfigurationShared::Builder& builder) {
for (auto* setting : Settings::values.linkage.by_category[category]) {
settings.push_back(setting);
}
+ for (auto* setting : UISettings::values.linkage.by_category[category]) {
+ settings.push_back(setting);
+ }
};
push(Settings::Category::Audio);
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index cbeb8f168..b22fda746 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -59,6 +59,8 @@ void ConfigureDebug::SetConfiguration() {
ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts.GetValue());
ui->use_auto_stub->setChecked(Settings::values.use_auto_stub.GetValue());
ui->enable_all_controllers->setChecked(Settings::values.enable_all_controllers.GetValue());
+ ui->enable_renderdoc_hotkey->setEnabled(runtime_lock);
+ ui->enable_renderdoc_hotkey->setChecked(Settings::values.enable_renderdoc_hotkey.GetValue());
ui->enable_graphics_debugging->setEnabled(runtime_lock);
ui->enable_graphics_debugging->setChecked(Settings::values.renderer_debug.GetValue());
ui->enable_shader_feedback->setEnabled(runtime_lock);
@@ -111,6 +113,7 @@ void ConfigureDebug::ApplyConfiguration() {
Settings::values.use_auto_stub = ui->use_auto_stub->isChecked();
Settings::values.enable_all_controllers = ui->enable_all_controllers->isChecked();
Settings::values.renderer_debug = ui->enable_graphics_debugging->isChecked();
+ Settings::values.enable_renderdoc_hotkey = ui->enable_renderdoc_hotkey->isChecked();
Settings::values.renderer_shader_feedback = ui->enable_shader_feedback->isChecked();
Settings::values.cpu_debug_mode = ui->enable_cpu_debugging->isChecked();
Settings::values.enable_nsight_aftermath = ui->enable_nsight_aftermath->isChecked();
diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui
index 97c7d9022..66b8b7459 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -18,8 +18,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>829</width>
- <height>758</height>
+ <width>842</width>
+ <height>741</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
@@ -260,7 +260,7 @@
<string>Graphics</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
- <item row="3" column="0">
+ <item row="4" column="0">
<widget class="QCheckBox" name="disable_loop_safety_checks">
<property name="toolTip">
<string>When checked, it executes shaders without loop logic changes</string>
@@ -270,33 +270,53 @@
</property>
</widget>
</item>
- <item row="4" column="0">
- <widget class="QCheckBox" name="dump_shaders">
+ <item row="8" column="0">
+ <widget class="QCheckBox" name="disable_macro_hle">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
- <string>When checked, it will dump all the original assembler shaders from the disk shader cache or game as found</string>
+ <string>When checked, it disables the macro HLE functions. Enabling this makes games run slower</string>
</property>
<property name="text">
- <string>Dump Game Shaders</string>
+ <string>Disable Macro HLE</string>
</property>
</widget>
</item>
<item row="7" column="0">
- <widget class="QCheckBox" name="disable_macro_hle">
+ <widget class="QCheckBox" name="dump_macros">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
- <string>When checked, it disables the macro HLE functions. Enabling this makes games run slower</string>
+ <string>When checked, it will dump all the macro programs of the GPU</string>
</property>
<property name="text">
- <string>Disable Macro HLE</string>
+ <string>Dump Maxwell Macros</string>
</property>
</widget>
</item>
- <item row="5" column="0">
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="enable_nsight_aftermath">
+ <property name="toolTip">
+ <string>When checked, it enables Nsight Aftermath crash dumps</string>
+ </property>
+ <property name="text">
+ <string>Enable Nsight Aftermath</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="enable_shader_feedback">
+ <property name="toolTip">
+ <string>When checked, yuzu will log statistics about the compiled pipeline cache</string>
+ </property>
+ <property name="text">
+ <string>Enable Shader Feedback</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
<widget class="QCheckBox" name="disable_macro_jit">
<property name="enabled">
<bool>true</bool>
@@ -309,6 +329,22 @@
</property>
</widget>
</item>
+ <item row="9" column="0">
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Preferred</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
<item row="0" column="0">
<widget class="QCheckBox" name="enable_graphics_debugging">
<property name="enabled">
@@ -322,55 +358,26 @@
</property>
</widget>
</item>
- <item row="6" column="0">
- <widget class="QCheckBox" name="dump_macros">
+ <item row="5" column="0">
+ <widget class="QCheckBox" name="dump_shaders">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
- <string>When checked, it will dump all the macro programs of the GPU</string>
+ <string>When checked, it will dump all the original assembler shaders from the disk shader cache or game as found</string>
</property>
<property name="text">
- <string>Dump Maxwell Macros</string>
+ <string>Dump Game Shaders</string>
</property>
</widget>
</item>
<item row="1" column="0">
- <widget class="QCheckBox" name="enable_shader_feedback">
- <property name="toolTip">
- <string>When checked, yuzu will log statistics about the compiled pipeline cache</string>
- </property>
- <property name="text">
- <string>Enable Shader Feedback</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QCheckBox" name="enable_nsight_aftermath">
- <property name="toolTip">
- <string>When checked, it enables Nsight Aftermath crash dumps</string>
- </property>
+ <widget class="QCheckBox" name="enable_renderdoc_hotkey">
<property name="text">
- <string>Enable Nsight Aftermath</string>
+ <string>Enable Renderdoc Hotkey</string>
</property>
</widget>
</item>
- <item row="8" column="0">
- <spacer name="verticalSpacer_5">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Preferred</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>0</height>
- </size>
- </property>
- </spacer>
- </item>
</layout>
</widget>
</item>
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index 8622dc184..fd6bebf0f 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -193,14 +193,10 @@ void ConfigureGraphics::PopulateVSyncModeSelection() {
: vsync_mode_combobox_enum_map[current_index];
int index{};
const int device{vulkan_device_combobox->currentIndex()}; //< current selected Vulkan device
- if (device == -1) {
- // Invalid device
- return;
- }
const auto& present_modes = //< relevant vector of present modes for the selected device or API
- backend == Settings::RendererBackend::Vulkan ? device_present_modes[device]
- : default_present_modes;
+ backend == Settings::RendererBackend::Vulkan && device > -1 ? device_present_modes[device]
+ : default_present_modes;
vsync_mode_combobox->clear();
vsync_mode_combobox_enum_map.clear();
@@ -497,11 +493,19 @@ void ConfigureGraphics::RetrieveVulkanDevices() {
}
Settings::RendererBackend ConfigureGraphics::GetCurrentGraphicsBackend() const {
- if (!Settings::IsConfiguringGlobal() && !api_restore_global_button->isEnabled()) {
- return Settings::values.renderer_backend.GetValue(true);
+ const auto selected_backend = [&]() {
+ if (!Settings::IsConfiguringGlobal() && !api_restore_global_button->isEnabled()) {
+ return Settings::values.renderer_backend.GetValue(true);
+ }
+ return static_cast<Settings::RendererBackend>(
+ combobox_translations.at(Settings::EnumMetadata<Settings::RendererBackend>::Index())
+ .at(api_combobox->currentIndex())
+ .first);
+ }();
+
+ if (selected_backend == Settings::RendererBackend::Vulkan &&
+ UISettings::values.has_broken_vulkan) {
+ return Settings::RendererBackend::OpenGL;
}
- return static_cast<Settings::RendererBackend>(
- combobox_translations.at(Settings::EnumMetadata<Settings::RendererBackend>::Index())
- .at(api_combobox->currentIndex())
- .first);
+ return selected_backend;
}
diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp
index 7fce85bca..5a48e388b 100644
--- a/src/yuzu/configuration/configure_input.cpp
+++ b/src/yuzu/configuration/configure_input.cpp
@@ -4,6 +4,8 @@
#include <memory>
#include <thread>
+#include "common/settings.h"
+#include "common/settings_enums.h"
#include "core/core.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
@@ -113,17 +115,9 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
for (std::size_t i = 0; i < player_tabs.size(); ++i) {
player_tabs[i]->setLayout(new QHBoxLayout(player_tabs[i]));
player_tabs[i]->layout()->addWidget(player_controllers[i]);
- connect(player_controllers[i], &ConfigureInputPlayer::Connected, [&, i](bool is_connected) {
+ connect(player_connected[i], &QCheckBox::clicked, [this, i](int checked) {
// Ensures that the controllers are always connected in sequential order
- if (is_connected) {
- for (std::size_t index = 0; index <= i; ++index) {
- player_connected[index]->setChecked(is_connected);
- }
- } else {
- for (std::size_t index = i; index < player_tabs.size(); ++index) {
- player_connected[index]->setChecked(is_connected);
- }
- }
+ this->propagateMouseClickOnPlayers(i, checked, true);
});
connect(player_controllers[i], &ConfigureInputPlayer::RefreshInputDevices, this,
&ConfigureInput::UpdateAllInputDevices);
@@ -181,6 +175,30 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
LoadConfiguration();
}
+void ConfigureInput::propagateMouseClickOnPlayers(size_t player_index, bool checked, bool origin) {
+ // Origin has already been toggled
+ if (!origin) {
+ player_connected[player_index]->setChecked(checked);
+ }
+
+ if (checked) {
+ // Check all previous buttons when checked
+ if (player_index > 0) {
+ propagateMouseClickOnPlayers(player_index - 1, checked, false);
+ }
+ } else {
+ // Unchecked all following buttons when unchecked
+ if (player_index < player_tabs.size() - 1) {
+ // Reconnect current player if it was the last one checked
+ // (player number was reduced by more than one)
+ if (origin && player_connected[player_index + 1]->checkState() == Qt::Checked) {
+ player_connected[player_index]->setCheckState(Qt::Checked);
+ }
+ propagateMouseClickOnPlayers(player_index + 1, checked, false);
+ }
+ }
+}
+
QList<QWidget*> ConfigureInput::GetSubTabs() const {
return {
ui->tabPlayer1, ui->tabPlayer2, ui->tabPlayer3, ui->tabPlayer4, ui->tabPlayer5,
@@ -197,9 +215,11 @@ void ConfigureInput::ApplyConfiguration() {
advanced->ApplyConfiguration();
- 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(), system);
+ const bool pre_docked_mode = Settings::IsDockedMode();
+ const bool docked_mode_selected = ui->radioDocked->isChecked();
+ Settings::values.use_docked_mode.SetValue(
+ docked_mode_selected ? Settings::ConsoleMode::Docked : Settings::ConsoleMode::Handheld);
+ OnDockedModeChanged(pre_docked_mode, docked_mode_selected, system);
Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked());
Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked());
@@ -267,8 +287,8 @@ void ConfigureInput::UpdateDockedState(bool is_handheld) {
ui->radioDocked->setEnabled(!is_handheld);
ui->radioUndocked->setEnabled(!is_handheld);
- ui->radioDocked->setChecked(Settings::values.use_docked_mode.GetValue());
- ui->radioUndocked->setChecked(!Settings::values.use_docked_mode.GetValue());
+ ui->radioDocked->setChecked(Settings::IsDockedMode());
+ ui->radioUndocked->setChecked(!Settings::IsDockedMode());
// Also force into undocked mode if the controller type is handheld.
if (is_handheld) {
diff --git a/src/yuzu/configuration/configure_input.h b/src/yuzu/configuration/configure_input.h
index c89189c36..abb7f7089 100644
--- a/src/yuzu/configuration/configure_input.h
+++ b/src/yuzu/configuration/configure_input.h
@@ -56,6 +56,7 @@ private:
void UpdateDockedState(bool is_handheld);
void UpdateAllInputDevices();
void UpdateAllInputProfiles(std::size_t player_index);
+ void propagateMouseClickOnPlayers(size_t player_index, bool origin, bool checked);
/// Load configuration settings.
void LoadConfiguration();
diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp
index 4f9e8db08..b91d6ad4a 100644
--- a/src/yuzu/configuration/configure_per_game.cpp
+++ b/src/yuzu/configuration/configure_per_game.cpp
@@ -18,6 +18,7 @@
#include "common/fs/fs_util.h"
#include "common/settings_enums.h"
+#include "common/settings_input.h"
#include "configuration/shared_widget.h"
#include "core/core.h"
#include "core/file_sys/control_metadata.h"
@@ -98,6 +99,12 @@ void ConfigurePerGame::ApplyConfiguration() {
addons_tab->ApplyConfiguration();
input_tab->ApplyConfiguration();
+ if (Settings::IsDockedMode() && Settings::values.players.GetValue()[0].controller_type ==
+ Settings::ControllerType::Handheld) {
+ Settings::values.use_docked_mode.SetValue(Settings::ConsoleMode::Handheld);
+ Settings::values.use_docked_mode.SetGlobal(true);
+ }
+
system.ApplySettings();
Settings::LogSettings();
diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp
index c4833f4e7..0c8e5c8b4 100644
--- a/src/yuzu/configuration/configure_system.cpp
+++ b/src/yuzu/configuration/configure_system.cpp
@@ -106,6 +106,11 @@ void ConfigureSystem::Setup(const ConfigurationShared::Builder& builder) {
push(Settings::values.linkage.by_category[Settings::Category::System]);
for (auto setting : settings) {
+ if (setting->Id() == Settings::values.use_docked_mode.Id() &&
+ Settings::IsConfiguringGlobal()) {
+ continue;
+ }
+
ConfigurationShared::Widget* widget = builder.BuildWidget(setting, apply_funcs);
if (widget == nullptr) {
diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp
index 34ab01617..82f3b6e78 100644
--- a/src/yuzu/configuration/configure_ui.cpp
+++ b/src/yuzu/configuration/configure_ui.cpp
@@ -4,6 +4,7 @@
#include "yuzu/configuration/configure_ui.h"
#include <array>
+#include <cstdlib>
#include <set>
#include <stdexcept>
#include <string>
@@ -94,11 +95,7 @@ static void PopulateResolutionComboBox(QComboBox* screenshot_height, QWidget* pa
}
static u32 ScreenshotDimensionToInt(const QString& height) {
- try {
- return std::stoi(height.toStdString());
- } catch (std::invalid_argument&) {
- return 0;
- }
+ return std::strtoul(height.toUtf8(), nullptr, 0);
}
ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
@@ -126,6 +123,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
+ connect(ui->show_play_time, &QCheckBox::stateChanged, this,
+ &ConfigureUi::RequestGameListUpdate);
connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ConfigureUi::RequestGameListUpdate);
connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged),
@@ -170,6 +169,7 @@ void ConfigureUi::ApplyConfiguration() {
UISettings::values.show_compat = ui->show_compat->isChecked();
UISettings::values.show_size = ui->show_size->isChecked();
UISettings::values.show_types = ui->show_types->isChecked();
+ UISettings::values.show_play_time = ui->show_play_time->isChecked();
UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt();
UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt();
UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt();
@@ -182,6 +182,7 @@ void ConfigureUi::ApplyConfiguration() {
const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText());
UISettings::values.screenshot_height.SetValue(height);
+ RequestGameListUpdate();
system.ApplySettings();
}
@@ -197,6 +198,7 @@ void ConfigureUi::SetConfiguration() {
ui->show_compat->setChecked(UISettings::values.show_compat.GetValue());
ui->show_size->setChecked(UISettings::values.show_size.GetValue());
ui->show_types->setChecked(UISettings::values.show_types.GetValue());
+ ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue());
ui->game_icon_size_combobox->setCurrentIndex(
ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue()));
ui->folder_icon_size_combobox->setCurrentIndex(
diff --git a/src/yuzu/configuration/configure_ui.ui b/src/yuzu/configuration/configure_ui.ui
index cb66ef104..b8e648381 100644
--- a/src/yuzu/configuration/configure_ui.ui
+++ b/src/yuzu/configuration/configure_ui.ui
@@ -105,6 +105,13 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="show_play_time">
+ <property name="text">
+ <string>Show Play Time Column</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2">
<item>
<widget class="QLabel" name="game_icon_size_label">
diff --git a/src/yuzu/configuration/shared_translation.cpp b/src/yuzu/configuration/shared_translation.cpp
index 335810788..3fe448f27 100644
--- a/src/yuzu/configuration/shared_translation.cpp
+++ b/src/yuzu/configuration/shared_translation.cpp
@@ -29,9 +29,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(Settings, sink_id, "Output Engine:", "");
INSERT(Settings, audio_output_device_id, "Output Device:", "");
INSERT(Settings, audio_input_device_id, "Input Device:", "");
- INSERT(Settings, audio_muted, "Mute audio when in background", "");
+ INSERT(Settings, audio_muted, "Mute audio", "");
INSERT(Settings, volume, "Volume:", "");
INSERT(Settings, dump_audio_commands, "", "");
+ INSERT(UISettings, mute_when_in_background, "Mute audio when in background", "");
// Core
INSERT(Settings, use_multi_core, "Multicore CPU Emulation", "");
@@ -135,7 +136,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(Settings, region_index, "Region:", "");
INSERT(Settings, time_zone_index, "Time Zone:", "");
INSERT(Settings, sound_index, "Sound Output Mode:", "");
- INSERT(Settings, use_docked_mode, "", "");
+ INSERT(Settings, use_docked_mode, "Console Mode:", "");
INSERT(Settings, current_user, "", "");
// Controls
@@ -156,6 +157,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(UISettings, select_user_on_boot, "Prompt for user on game boot", "");
INSERT(UISettings, pause_when_in_background, "Pause emulation when in background", "");
INSERT(UISettings, confirm_before_closing, "Confirm exit while emulation is running", "");
+ INSERT(UISettings, confirm_before_stopping, "Confirm before stopping emulation", "");
INSERT(UISettings, hide_mouse, "Hide mouse on inactivity", "");
INSERT(UISettings, controller_applet_disabled, "Disable controller applet", "");
@@ -379,6 +381,16 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
PAIR(MemoryLayout, Memory_6Gb, "6GB DRAM (Unsafe)"),
PAIR(MemoryLayout, Memory_8Gb, "8GB DRAM (Unsafe)"),
}});
+ translations->insert(
+ {Settings::EnumMetadata<Settings::ConsoleMode>::Index(),
+ {PAIR(ConsoleMode, Docked, "Docked"), PAIR(ConsoleMode, Handheld, "Handheld")}});
+ translations->insert(
+ {Settings::EnumMetadata<Settings::ConfirmStop>::Index(),
+ {
+ PAIR(ConfirmStop, Ask_Always, "Always ask (Default)"),
+ PAIR(ConfirmStop, Ask_Based_On_Game, "Only if game specifies not to stop"),
+ PAIR(ConfirmStop, Ask_Never, "Never ask"),
+ }});
#undef PAIR
#undef CTX_PAIR
diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp
index bdb38c8ea..ea8d7add4 100644
--- a/src/yuzu/configuration/shared_widget.cpp
+++ b/src/yuzu/configuration/shared_widget.cpp
@@ -23,6 +23,7 @@
#include <QLineEdit>
#include <QObject>
#include <QPushButton>
+#include <QRadioButton>
#include <QRegularExpression>
#include <QSizePolicy>
#include <QSlider>
@@ -62,7 +63,7 @@ static QString DefaultSuffix(QWidget* parent, Settings::BasicSetting& setting) {
return tr("%", context.c_str());
}
- return QStringLiteral("");
+ return default_suffix;
}
QPushButton* Widget::CreateRestoreGlobalButton(bool using_global, QWidget* parent) {
@@ -70,7 +71,7 @@ QPushButton* Widget::CreateRestoreGlobalButton(bool using_global, QWidget* paren
QStyle* style = parent->style();
QIcon* icon = new QIcon(style->standardIcon(QStyle::SP_LineEditClearButton));
- QPushButton* restore_button = new QPushButton(*icon, QStringLiteral(""), parent);
+ QPushButton* restore_button = new QPushButton(*icon, QStringLiteral(), parent);
restore_button->setObjectName(QStringLiteral("RestoreButton%1").arg(restore_button_count));
restore_button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
@@ -150,7 +151,7 @@ QWidget* Widget::CreateCombobox(std::function<std::string()>& serializer,
return -1;
};
- const u32 setting_value = std::stoi(setting.ToString());
+ const u32 setting_value = std::strtoul(setting.ToString().c_str(), nullptr, 0);
combobox->setCurrentIndex(find_index(setting_value));
serializer = [this, enumeration]() {
@@ -159,7 +160,7 @@ QWidget* Widget::CreateCombobox(std::function<std::string()>& serializer,
};
restore_func = [this, find_index]() {
- const u32 global_value = std::stoi(RelevantDefault(setting));
+ const u32 global_value = std::strtoul(RelevantDefault(setting).c_str(), nullptr, 0);
combobox->setCurrentIndex(find_index(global_value));
};
@@ -171,6 +172,65 @@ QWidget* Widget::CreateCombobox(std::function<std::string()>& serializer,
return combobox;
}
+QWidget* Widget::CreateRadioGroup(std::function<std::string()>& serializer,
+ std::function<void()>& restore_func,
+ const std::function<void()>& touch) {
+ const auto type = setting.EnumIndex();
+
+ QWidget* group = new QWidget(this);
+ QHBoxLayout* layout = new QHBoxLayout(group);
+ layout->setContentsMargins(0, 0, 0, 0);
+ group->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+
+ const ComboboxTranslations* enumeration{nullptr};
+ if (combobox_enumerations.contains(type)) {
+ enumeration = &combobox_enumerations.at(type);
+ for (const auto& [id, name] : *enumeration) {
+ QRadioButton* radio_button = new QRadioButton(name, group);
+ layout->addWidget(radio_button);
+ radio_buttons.push_back({id, radio_button});
+ }
+ } else {
+ return group;
+ }
+
+ const auto get_selected = [=]() -> int {
+ for (const auto& [id, button] : radio_buttons) {
+ if (button->isChecked()) {
+ return id;
+ }
+ }
+ return -1;
+ };
+
+ const auto set_index = [=](u32 value) {
+ for (const auto& [id, button] : radio_buttons) {
+ button->setChecked(id == value);
+ }
+ };
+
+ const u32 setting_value = std::strtoul(setting.ToString().c_str(), nullptr, 0);
+ set_index(setting_value);
+
+ serializer = [get_selected]() {
+ int current = get_selected();
+ return std::to_string(current);
+ };
+
+ restore_func = [this, set_index]() {
+ const u32 global_value = std::strtoul(RelevantDefault(setting).c_str(), nullptr, 0);
+ set_index(global_value);
+ };
+
+ if (!Settings::IsConfiguringGlobal()) {
+ for (const auto& [id, button] : radio_buttons) {
+ QObject::connect(button, &QAbstractButton::clicked, [touch]() { touch(); });
+ }
+ }
+
+ return group;
+}
+
QWidget* Widget::CreateLineEdit(std::function<std::string()>& serializer,
std::function<void()>& restore_func,
const std::function<void()>& touch, bool managed) {
@@ -195,6 +255,59 @@ QWidget* Widget::CreateLineEdit(std::function<std::string()>& serializer,
return line_edit;
}
+static void CreateIntSlider(Settings::BasicSetting& setting, bool reversed, float multiplier,
+ QLabel* feedback, const QString& use_format, QSlider* slider,
+ std::function<std::string()>& serializer,
+ std::function<void()>& restore_func) {
+ const int max_val = std::strtol(setting.MaxVal().c_str(), nullptr, 0);
+
+ const auto update_feedback = [=](int value) {
+ int present = (reversed ? max_val - value : value) * multiplier + 0.5f;
+ feedback->setText(use_format.arg(QVariant::fromValue(present).value<QString>()));
+ };
+
+ QObject::connect(slider, &QAbstractSlider::valueChanged, update_feedback);
+ update_feedback(std::strtol(setting.ToString().c_str(), nullptr, 0));
+
+ slider->setMinimum(std::strtol(setting.MinVal().c_str(), nullptr, 0));
+ slider->setMaximum(max_val);
+ slider->setValue(std::strtol(setting.ToString().c_str(), nullptr, 0));
+
+ serializer = [slider]() { return std::to_string(slider->value()); };
+ restore_func = [slider, &setting]() {
+ slider->setValue(std::strtol(RelevantDefault(setting).c_str(), nullptr, 0));
+ };
+}
+
+static void CreateFloatSlider(Settings::BasicSetting& setting, bool reversed, float multiplier,
+ QLabel* feedback, const QString& use_format, QSlider* slider,
+ std::function<std::string()>& serializer,
+ std::function<void()>& restore_func) {
+ const float max_val = std::strtof(setting.MaxVal().c_str(), nullptr);
+ const float min_val = std::strtof(setting.MinVal().c_str(), nullptr);
+ const float use_multiplier =
+ multiplier == default_multiplier ? default_float_multiplier : multiplier;
+
+ const auto update_feedback = [=](float value) {
+ int present = (reversed ? max_val - value : value) + 0.5f;
+ feedback->setText(use_format.arg(QVariant::fromValue(present).value<QString>()));
+ };
+
+ QObject::connect(slider, &QAbstractSlider::valueChanged, update_feedback);
+ update_feedback(std::strtof(setting.ToString().c_str(), nullptr));
+
+ slider->setMinimum(min_val * use_multiplier);
+ slider->setMaximum(max_val * use_multiplier);
+ slider->setValue(std::strtof(setting.ToString().c_str(), nullptr) * use_multiplier);
+
+ serializer = [slider, use_multiplier]() {
+ return std::to_string(slider->value() / use_multiplier);
+ };
+ restore_func = [slider, &setting, use_multiplier]() {
+ slider->setValue(std::strtof(RelevantDefault(setting).c_str(), nullptr) * use_multiplier);
+ };
+}
+
QWidget* Widget::CreateSlider(bool reversed, float multiplier, const QString& given_suffix,
std::function<std::string()>& serializer,
std::function<void()>& restore_func,
@@ -218,27 +331,20 @@ QWidget* Widget::CreateSlider(bool reversed, float multiplier, const QString& gi
layout->setContentsMargins(0, 0, 0, 0);
- int max_val = std::stoi(setting.MaxVal());
-
- QString suffix =
- given_suffix == QStringLiteral("") ? DefaultSuffix(this, setting) : given_suffix;
+ QString suffix = given_suffix == default_suffix ? DefaultSuffix(this, setting) : given_suffix;
const QString use_format = QStringLiteral("%1").append(suffix);
- QObject::connect(slider, &QAbstractSlider::valueChanged, [=](int value) {
- int present = (reversed ? max_val - value : value) * multiplier + 0.5f;
- feedback->setText(use_format.arg(QVariant::fromValue(present).value<QString>()));
- });
-
- slider->setMinimum(std::stoi(setting.MinVal()));
- slider->setMaximum(max_val);
- slider->setValue(std::stoi(setting.ToString()));
+ if (setting.IsIntegral()) {
+ CreateIntSlider(setting, reversed, multiplier, feedback, use_format, slider, serializer,
+ restore_func);
+ } else {
+ CreateFloatSlider(setting, reversed, multiplier, feedback, use_format, slider, serializer,
+ restore_func);
+ }
slider->setInvertedAppearance(reversed);
- serializer = [this]() { return std::to_string(slider->value()); };
- restore_func = [this]() { slider->setValue(std::stoi(RelevantDefault(setting))); };
-
if (!Settings::IsConfiguringGlobal()) {
QObject::connect(slider, &QAbstractSlider::actionTriggered, [touch]() { touch(); });
}
@@ -250,14 +356,11 @@ QWidget* Widget::CreateSpinBox(const QString& given_suffix,
std::function<std::string()>& serializer,
std::function<void()>& restore_func,
const std::function<void()>& touch) {
- const int min_val =
- setting.Ranged() ? std::stoi(setting.MinVal()) : std::numeric_limits<int>::min();
- const int max_val =
- setting.Ranged() ? std::stoi(setting.MaxVal()) : std::numeric_limits<int>::max();
- const int default_val = std::stoi(setting.ToString());
+ const auto min_val = std::strtol(setting.MinVal().c_str(), nullptr, 0);
+ const auto max_val = std::strtol(setting.MaxVal().c_str(), nullptr, 0);
+ const auto default_val = std::strtol(setting.ToString().c_str(), nullptr, 0);
- QString suffix =
- given_suffix == QStringLiteral("") ? DefaultSuffix(this, setting) : given_suffix;
+ QString suffix = given_suffix == default_suffix ? DefaultSuffix(this, setting) : given_suffix;
spinbox = new QSpinBox(this);
spinbox->setRange(min_val, max_val);
@@ -268,13 +371,13 @@ QWidget* Widget::CreateSpinBox(const QString& given_suffix,
serializer = [this]() { return std::to_string(spinbox->value()); };
restore_func = [this]() {
- auto value{std::stol(RelevantDefault(setting))};
+ auto value{std::strtol(RelevantDefault(setting).c_str(), nullptr, 0)};
spinbox->setValue(value);
};
if (!Settings::IsConfiguringGlobal()) {
QObject::connect(spinbox, QOverload<int>::of(&QSpinBox::valueChanged), [this, touch]() {
- if (spinbox->value() != std::stoi(setting.ToStringGlobal())) {
+ if (spinbox->value() != std::strtol(setting.ToStringGlobal().c_str(), nullptr, 0)) {
touch();
}
});
@@ -283,6 +386,42 @@ QWidget* Widget::CreateSpinBox(const QString& given_suffix,
return spinbox;
}
+QWidget* Widget::CreateDoubleSpinBox(const QString& given_suffix,
+ std::function<std::string()>& serializer,
+ std::function<void()>& restore_func,
+ const std::function<void()>& touch) {
+ const auto min_val = std::strtod(setting.MinVal().c_str(), nullptr);
+ const auto max_val = std::strtod(setting.MaxVal().c_str(), nullptr);
+ const auto default_val = std::strtod(setting.ToString().c_str(), nullptr);
+
+ QString suffix = given_suffix == default_suffix ? DefaultSuffix(this, setting) : given_suffix;
+
+ double_spinbox = new QDoubleSpinBox(this);
+ double_spinbox->setRange(min_val, max_val);
+ double_spinbox->setValue(default_val);
+ double_spinbox->setSuffix(suffix);
+ double_spinbox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+
+ serializer = [this]() { return fmt::format("{:f}", double_spinbox->value()); };
+
+ restore_func = [this]() {
+ auto value{std::strtod(RelevantDefault(setting).c_str(), nullptr)};
+ double_spinbox->setValue(value);
+ };
+
+ if (!Settings::IsConfiguringGlobal()) {
+ QObject::connect(double_spinbox, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ [this, touch]() {
+ if (double_spinbox->value() !=
+ std::strtod(setting.ToStringGlobal().c_str(), nullptr)) {
+ touch();
+ }
+ });
+ }
+
+ return double_spinbox;
+}
+
QWidget* Widget::CreateHexEdit(std::function<std::string()>& serializer,
std::function<void()>& restore_func,
const std::function<void()>& touch) {
@@ -292,7 +431,8 @@ QWidget* Widget::CreateHexEdit(std::function<std::string()>& serializer,
}
auto to_hex = [=](const std::string& input) {
- return QString::fromStdString(fmt::format("{:08x}", std::stoul(input)));
+ return QString::fromStdString(
+ fmt::format("{:08x}", std::strtoul(input.c_str(), nullptr, 0)));
};
QRegularExpressionValidator* regex = new QRegularExpressionValidator(
@@ -305,7 +445,7 @@ QWidget* Widget::CreateHexEdit(std::function<std::string()>& serializer,
line_edit->setValidator(regex);
auto hex_to_dec = [this]() -> std::string {
- return std::to_string(std::stoul(line_edit->text().toStdString(), nullptr, 16));
+ return std::to_string(std::strtoul(line_edit->text().toStdString().c_str(), nullptr, 16));
};
serializer = [hex_to_dec]() { return hex_to_dec(); };
@@ -325,7 +465,8 @@ QWidget* Widget::CreateDateTimeEdit(bool disabled, bool restrict,
std::function<void()>& restore_func,
const std::function<void()>& touch) {
const long long current_time = QDateTime::currentSecsSinceEpoch();
- const s64 the_time = disabled ? current_time : std::stoll(setting.ToString());
+ const s64 the_time =
+ disabled ? current_time : std::strtoll(setting.ToString().c_str(), nullptr, 0);
const auto default_val = QDateTime::fromSecsSinceEpoch(the_time);
date_time_edit = new QDateTimeEdit(this);
@@ -338,7 +479,7 @@ QWidget* Widget::CreateDateTimeEdit(bool disabled, bool restrict,
auto get_clear_val = [this, restrict, current_time]() {
return QDateTime::fromSecsSinceEpoch([this, restrict, current_time]() {
if (restrict && checkbox->checkState() == Qt::Checked) {
- return std::stoll(RelevantDefault(setting));
+ return std::strtoll(RelevantDefault(setting).c_str(), nullptr, 0);
}
return current_time;
}());
@@ -410,6 +551,8 @@ void Widget::SetupComponent(const QString& label, std::function<void()>& load_fu
return RequestType::Slider;
case Settings::Specialization::Countable:
return RequestType::SpinBox;
+ case Settings::Specialization::Radio:
+ return RequestType::RadioGroup;
default:
break;
}
@@ -438,9 +581,12 @@ void Widget::SetupComponent(const QString& label, std::function<void()>& load_fu
if (setting.TypeId() == typeid(bool)) {
data_component = CreateCheckBox(&setting, label, serializer, restore_func, touch);
} else if (setting.IsEnum()) {
- data_component = CreateCombobox(serializer, restore_func, touch);
- } else if (type == typeid(u32) || type == typeid(int) || type == typeid(u16) ||
- type == typeid(s64) || type == typeid(u8)) {
+ if (request == RequestType::RadioGroup) {
+ data_component = CreateRadioGroup(serializer, restore_func, touch);
+ } else {
+ data_component = CreateCombobox(serializer, restore_func, touch);
+ }
+ } else if (setting.IsIntegral()) {
switch (request) {
case RequestType::Slider:
case RequestType::ReverseSlider:
@@ -467,6 +613,20 @@ void Widget::SetupComponent(const QString& label, std::function<void()>& load_fu
default:
UNIMPLEMENTED();
}
+ } else if (setting.IsFloatingPoint()) {
+ switch (request) {
+ case RequestType::Default:
+ case RequestType::SpinBox:
+ data_component = CreateDoubleSpinBox(suffix, serializer, restore_func, touch);
+ break;
+ case RequestType::Slider:
+ case RequestType::ReverseSlider:
+ data_component = CreateSlider(request == RequestType::ReverseSlider, multiplier, suffix,
+ serializer, restore_func, touch);
+ break;
+ default:
+ UNIMPLEMENTED();
+ }
} else if (type == typeid(std::string)) {
switch (request) {
case RequestType::Default:
@@ -571,10 +731,10 @@ Widget::Widget(Settings::BasicSetting* setting_, const TranslationMap& translati
return std::pair{translations.at(id).first, translations.at(id).second};
}
LOG_WARNING(Frontend, "Translation table lacks entry for \"{}\"", setting_label);
- return std::pair{QString::fromStdString(setting_label), QStringLiteral("")};
+ return std::pair{QString::fromStdString(setting_label), QStringLiteral()};
}();
- if (label == QStringLiteral("")) {
+ if (label == QStringLiteral()) {
LOG_DEBUG(Frontend, "Translation table has empty entry for \"{}\", skipping...",
setting.GetLabel());
return;
diff --git a/src/yuzu/configuration/shared_widget.h b/src/yuzu/configuration/shared_widget.h
index e64693bab..226284cf3 100644
--- a/src/yuzu/configuration/shared_widget.h
+++ b/src/yuzu/configuration/shared_widget.h
@@ -22,6 +22,8 @@ class QObject;
class QPushButton;
class QSlider;
class QSpinBox;
+class QDoubleSpinBox;
+class QRadioButton;
namespace Settings {
class BasicSetting;
@@ -38,9 +40,14 @@ enum class RequestType {
LineEdit,
HexEdit,
DateTimeEdit,
+ RadioGroup,
MaxEnum,
};
+constexpr float default_multiplier{1.f};
+constexpr float default_float_multiplier{100.f};
+static const QString default_suffix = QStringLiteral();
+
class Widget : public QWidget {
Q_OBJECT
@@ -64,8 +71,9 @@ public:
const ComboboxTranslationMap& combobox_translations, QWidget* parent,
bool runtime_lock, std::vector<std::function<void(bool)>>& apply_funcs_,
RequestType request = RequestType::Default, bool managed = true,
- float multiplier = 1.0f, Settings::BasicSetting* other_setting = nullptr,
- const QString& suffix = QStringLiteral(""));
+ float multiplier = default_multiplier,
+ Settings::BasicSetting* other_setting = nullptr,
+ const QString& suffix = default_suffix);
virtual ~Widget();
/**
@@ -87,10 +95,12 @@ public:
QPushButton* restore_button{}; ///< Restore button for custom configurations
QLineEdit* line_edit{}; ///< QLineEdit, used for LineEdit and HexEdit
QSpinBox* spinbox{};
+ QDoubleSpinBox* double_spinbox{};
QCheckBox* checkbox{};
QSlider* slider{};
QComboBox* combobox{};
QDateTimeEdit* date_time_edit{};
+ std::vector<std::pair<u32, QRadioButton*>> radio_buttons{};
private:
void SetupComponent(const QString& label, std::function<void()>& load_func, bool managed,
@@ -106,6 +116,9 @@ private:
QWidget* CreateCombobox(std::function<std::string()>& serializer,
std::function<void()>& restore_func,
const std::function<void()>& touch);
+ QWidget* CreateRadioGroup(std::function<std::string()>& serializer,
+ std::function<void()>& restore_func,
+ const std::function<void()>& touch);
QWidget* CreateLineEdit(std::function<std::string()>& serializer,
std::function<void()>& restore_func, const std::function<void()>& touch,
bool managed = true);
@@ -120,6 +133,9 @@ private:
const std::function<void()>& touch);
QWidget* CreateSpinBox(const QString& suffix, std::function<std::string()>& serializer,
std::function<void()>& restore_func, const std::function<void()>& touch);
+ QWidget* CreateDoubleSpinBox(const QString& suffix, std::function<std::string()>& serializer,
+ std::function<void()>& restore_func,
+ const std::function<void()>& touch);
QWidget* parent;
const TranslationMap& translations;
@@ -139,14 +155,15 @@ public:
Widget* BuildWidget(Settings::BasicSetting* setting,
std::vector<std::function<void(bool)>>& apply_funcs,
RequestType request = RequestType::Default, bool managed = true,
- float multiplier = 1.0f, Settings::BasicSetting* other_setting = nullptr,
- const QString& suffix = QStringLiteral("")) const;
+ float multiplier = default_multiplier,
+ Settings::BasicSetting* other_setting = nullptr,
+ const QString& suffix = default_suffix) const;
Widget* BuildWidget(Settings::BasicSetting* setting,
std::vector<std::function<void(bool)>>& apply_funcs,
Settings::BasicSetting* other_setting,
RequestType request = RequestType::Default,
- const QString& suffix = QStringLiteral("")) const;
+ const QString& suffix = default_suffix) const;
const ComboboxTranslationMap& ComboboxTranslations() const;
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index b5a02700d..2bb1a0239 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -312,8 +312,10 @@ void GameList::OnFilterCloseClicked() {
}
GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_,
- Core::System& system_, GMainWindow* parent)
- : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, system{system_} {
+ PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
+ GMainWindow* parent)
+ : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
+ play_time_manager{play_time_manager_}, system{system_} {
watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
@@ -340,6 +342,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
+ tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
item_model->setSortRole(GameListItemPath::SortRole);
connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
@@ -548,6 +551,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC"));
QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration"));
+ QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data"));
QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage"));
QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache"));
QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache"));
@@ -557,11 +561,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS"));
QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS"));
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
+ QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
-#ifndef WIN32
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
+#ifndef WIN32
QAction* create_applications_menu_shortcut =
shortcut_menu->addAction(tr("Add to Applications Menu"));
#endif
@@ -588,10 +593,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path);
});
connect(start_game, &QAction::triggered, [this, path]() {
- emit BootGame(QString::fromStdString(path), 0, 0, StartGameType::Normal);
+ emit BootGame(QString::fromStdString(path), 0, 0, StartGameType::Normal,
+ AmLaunchType::UserInitiated);
});
connect(start_game_global, &QAction::triggered, [this, path]() {
- emit BootGame(QString::fromStdString(path), 0, 0, StartGameType::Global);
+ emit BootGame(QString::fromStdString(path), 0, 0, StartGameType::Global,
+ AmLaunchType::UserInitiated);
});
connect(open_mod_location, &QAction::triggered, [this, program_id, path]() {
emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path);
@@ -619,6 +626,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path);
});
+ connect(remove_play_time_data, &QAction::triggered,
+ [this, program_id]() { emit RemovePlayTimeRequested(program_id); });
connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path);
});
@@ -628,15 +637,17 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path]() {
emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::SDMC);
});
+ connect(verify_integrity, &QAction::triggered,
+ [this, path]() { emit VerifyIntegrityRequested(path); });
connect(copy_tid, &QAction::triggered,
[this, program_id]() { emit CopyTIDRequested(program_id); });
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
});
-#ifndef WIN32
connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);
});
+#ifndef WIN32
connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);
});
@@ -785,6 +796,7 @@ void GameList::RetranslateUI() {
item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
+ item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
}
void GameListSearchField::changeEvent(QEvent* event) {
@@ -812,15 +824,17 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
+ tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
+
+ // Before deleting rows, cancel the worker so that it is not using them
+ emit ShouldCancelWorker();
// Delete any rows that might already exist if we're repopulating
item_model->removeRows(0, item_model->rowCount());
search_field->clear();
- emit ShouldCancelWorker();
-
GameListWorker* worker =
- new GameListWorker(vfs, provider, game_dirs, compatibility_list, system);
+ new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index 6c2f75e53..712570cea 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -18,6 +18,7 @@
#include "core/core.h"
#include "uisettings.h"
#include "yuzu/compatibility_list.h"
+#include "yuzu/play_time_manager.h"
namespace Core {
class System;
@@ -28,6 +29,7 @@ class GameListWorker;
class GameListSearchField;
class GameListDir;
class GMainWindow;
+enum class AmLaunchType;
enum class StartGameType;
namespace FileSys {
@@ -74,11 +76,13 @@ public:
COLUMN_ADD_ONS,
COLUMN_FILE_TYPE,
COLUMN_SIZE,
+ COLUMN_PLAY_TIME,
COLUMN_COUNT, // Number of columns
};
explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
- FileSys::ManualContentProvider* provider_, Core::System& system_,
+ FileSys::ManualContentProvider* provider_,
+ PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
GMainWindow* parent = nullptr);
~GameList() override;
@@ -103,7 +107,7 @@ public:
signals:
void BootGame(const QString& game_path, u64 program_id, std::size_t program_index,
- StartGameType type);
+ StartGameType type, AmLaunchType launch_type);
void GameChosen(const QString& game_path, const u64 title_id = 0);
void ShouldCancelWorker();
void OpenFolderRequested(u64 program_id, GameListOpenTarget target,
@@ -112,7 +116,9 @@ signals:
void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type);
void RemoveFileRequested(u64 program_id, GameListRemoveTarget target,
const std::string& game_path);
+ void RemovePlayTimeRequested(u64 program_id);
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
+ void VerifyIntegrityRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id);
void CreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target);
@@ -166,6 +172,7 @@ private:
friend class GameListSearchField;
+ const PlayTime::PlayTimeManager& play_time_manager;
Core::System& system;
};
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index 1800f090f..86a0c41d9 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -18,6 +18,7 @@
#include "common/common_types.h"
#include "common/logging/log.h"
#include "common/string_util.h"
+#include "yuzu/play_time_manager.h"
#include "yuzu/uisettings.h"
#include "yuzu/util/util.h"
@@ -221,6 +222,31 @@ public:
}
};
+/**
+ * GameListItem for Play Time values.
+ * This object stores the play time of a game in seconds, and its readable
+ * representation in minutes/hours
+ */
+class GameListItemPlayTime : public GameListItem {
+public:
+ static constexpr int PlayTimeRole = SortRole;
+
+ GameListItemPlayTime() = default;
+ explicit GameListItemPlayTime(const qulonglong time_seconds) {
+ setData(time_seconds, PlayTimeRole);
+ }
+
+ void setData(const QVariant& value, int role) override {
+ qulonglong time_seconds = value.toULongLong();
+ GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole);
+ GameListItem::setData(value, PlayTimeRole);
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
+ }
+};
+
class GameListDir : public GameListItem {
public:
static constexpr int GameDirRole = Qt::UserRole + 2;
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index 9404365b4..077ced12b 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -191,8 +191,10 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
}
QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
- const std::vector<u8>& icon, Loader::AppLoader& loader,
- u64 program_id, const CompatibilityList& compatibility_list,
+ const std::size_t size, const std::vector<u8>& icon,
+ Loader::AppLoader& loader, u64 program_id,
+ const CompatibilityList& compatibility_list,
+ const PlayTime::PlayTimeManager& play_time_manager,
const FileSys::PatchManager& patch) {
const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
@@ -210,7 +212,8 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
file_type_string, program_id),
new GameListItemCompat(compatibility),
new GameListItem(file_type_string),
- new GameListItemSize(Common::FS::GetSize(path)),
+ new GameListItemSize(size),
+ new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
};
const auto patch_versions = GetGameListCachedObject(
@@ -226,9 +229,12 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
FileSys::ManualContentProvider* provider_,
QVector<UISettings::GameDir>& game_dirs_,
- const CompatibilityList& compatibility_list_, Core::System& system_)
+ const CompatibilityList& compatibility_list_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_)
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
- compatibility_list{compatibility_list_}, system{system_} {}
+ compatibility_list{compatibility_list_},
+ play_time_manager{play_time_manager_}, system{system_} {}
GameListWorker::~GameListWorker() = default;
@@ -278,8 +284,8 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
GetMetadataFromControlNCA(patch, *control, icon, name);
}
- emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
- compatibility_list, patch),
+ emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
+ program_id, compatibility_list, play_time_manager, patch),
parent_dir);
}
}
@@ -287,7 +293,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
GameListDir* parent_dir) {
const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
- if (stop_processing) {
+ if (stop_requested) {
// Breaks the callback loop.
return false;
}
@@ -354,8 +360,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
system.GetContentProvider()};
- emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, id,
- compatibility_list, patch),
+ emit EntryReady(MakeGameListEntry(physical_name, name,
+ Common::FS::GetSize(physical_name), icon,
+ *loader, id, compatibility_list,
+ play_time_manager, patch),
parent_dir);
}
} else {
@@ -368,8 +376,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
system.GetContentProvider()};
- emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader,
- program_id, compatibility_list, patch),
+ emit EntryReady(MakeGameListEntry(physical_name, name,
+ Common::FS::GetSize(physical_name), icon,
+ *loader, program_id, compatibility_list,
+ play_time_manager, patch),
parent_dir);
}
}
@@ -389,7 +399,6 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
}
void GameListWorker::run() {
- stop_processing = false;
provider->ClearAllEntries();
for (UISettings::GameDir& game_dir : game_dirs) {
@@ -417,9 +426,11 @@ void GameListWorker::run() {
}
emit Finished(watch_list);
+ processing_completed.Set();
}
void GameListWorker::Cancel() {
this->disconnect();
- stop_processing = true;
+ stop_requested.store(true);
+ processing_completed.Wait();
}
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h
index 24a4e92c3..54dc05e30 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -12,7 +12,9 @@
#include <QRunnable>
#include <QString>
+#include "common/thread.h"
#include "yuzu/compatibility_list.h"
+#include "yuzu/play_time_manager.h"
namespace Core {
class System;
@@ -36,7 +38,9 @@ public:
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
FileSys::ManualContentProvider* provider_,
QVector<UISettings::GameDir>& game_dirs_,
- const CompatibilityList& compatibility_list_, Core::System& system_);
+ const CompatibilityList& compatibility_list_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_);
~GameListWorker() override;
/// Starts the processing of directory tree information.
@@ -76,9 +80,12 @@ private:
FileSys::ManualContentProvider* provider;
QVector<UISettings::GameDir>& game_dirs;
const CompatibilityList& compatibility_list;
+ const PlayTime::PlayTimeManager& play_time_manager;
QStringList watch_list;
- std::atomic_bool stop_processing;
+
+ Common::Event processing_completed;
+ std::atomic_bool stop_requested = false;
Core::System& system;
};
diff --git a/src/yuzu/hotkeys.h b/src/yuzu/hotkeys.h
index 848239c35..56eee8d82 100644
--- a/src/yuzu/hotkeys.h
+++ b/src/yuzu/hotkeys.h
@@ -4,10 +4,12 @@
#pragma once
#include <map>
+#include <QKeySequence>
+#include <QString>
+#include <QWidget>
#include "core/hid/hid_types.h"
class QDialog;
-class QKeySequence;
class QSettings;
class QShortcut;
class ControllerShortcut;
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index a9d035f3d..1431cf2fe 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -8,6 +8,8 @@
#include <iostream>
#include <memory>
#include <thread>
+#include "core/loader/nca.h"
+#include "core/tools/renderdoc.h"
#ifdef __APPLE__
#include <unistd.h> // for chdir
#endif
@@ -16,6 +18,8 @@
#include <sys/socket.h>
#endif
+#include <boost/container/flat_set.hpp>
+
// VFS includes must be before glad as they will conflict with Windows file api, which uses defines.
#include "applets/qt_amiibo_settings.h"
#include "applets/qt_controller.h"
@@ -63,6 +67,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#define QT_NO_OPENGL
#include <QClipboard>
#include <QDesktopServices>
+#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QInputDialog>
@@ -72,6 +77,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QPushButton>
#include <QScreen>
#include <QShortcut>
+#include <QStandardPaths>
#include <QStatusBar>
#include <QString>
#include <QSysInfo>
@@ -94,6 +100,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#ifdef _WIN32
+#include <shlobj.h>
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
@@ -146,6 +153,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/install_dialog.h"
#include "yuzu/loading_screen.h"
#include "yuzu/main.h"
+#include "yuzu/play_time_manager.h"
#include "yuzu/startup_checks.h"
#include "yuzu/uisettings.h"
#include "yuzu/util/clickable_label.h"
@@ -203,7 +211,7 @@ void GMainWindow::ShowTelemetryCallout() {
tr("<a href='https://yuzu-emu.org/help/feature/telemetry/'>Anonymous "
"data is collected</a> to help improve yuzu. "
"<br/><br/>Would you like to share your usage data with us?");
- if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) {
+ if (!question(this, tr("Telemetry"), telemetry_message)) {
Settings::values.enable_telemetry = false;
system->ApplySettings();
}
@@ -334,6 +342,8 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
discord_rpc->Update();
+ play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
+
system->GetRoomNetwork().Init();
RegisterMetaTypes();
@@ -442,8 +452,13 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
"#yuzu-starts-with-the-error-broken-vulkan-installation-detected'>"
"here for instructions to fix the issue</a>."));
+#ifdef HAS_OPENGL
Settings::values.renderer_backend = Settings::RendererBackend::OpenGL;
+#else
+ Settings::values.renderer_backend = Settings::RendererBackend::Null;
+#endif
+ UpdateAPIText();
renderer_status_button->setDisabled(true);
renderer_status_button->setChecked(false);
} else {
@@ -977,7 +992,7 @@ void GMainWindow::InitializeWidgets() {
render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system);
render_window->hide();
- game_list = new GameList(vfs, provider.get(), *system, this);
+ game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this);
ui->horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
@@ -1158,9 +1173,9 @@ void GMainWindow::InitializeWidgets() {
[this](const QPoint& menu_location) {
QMenu context_menu;
- for (auto const& docked_mode_pair : Config::use_docked_mode_texts_map) {
- context_menu.addAction(docked_mode_pair.second, [this, docked_mode_pair] {
- if (docked_mode_pair.first != Settings::values.use_docked_mode.GetValue()) {
+ for (auto const& pair : Config::use_docked_mode_texts_map) {
+ context_menu.addAction(pair.second, [this, &pair] {
+ if (pair.first != Settings::values.use_docked_mode.GetValue()) {
OnToggleDockedMode();
}
});
@@ -1342,6 +1357,11 @@ void GMainWindow::InitializeHotkeys() {
connect_shortcut(QStringLiteral("Toggle Framerate Limit"), [] {
Settings::values.use_speed_limit.SetValue(!Settings::values.use_speed_limit.GetValue());
});
+ connect_shortcut(QStringLiteral("Toggle Renderdoc Capture"), [this] {
+ if (Settings::values.enable_renderdoc_hotkey) {
+ system->GetRenderdocAPI().ToggleCapture();
+ }
+ });
connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
if (Settings::values.mouse_enabled) {
Settings::values.mouse_panning = false;
@@ -1433,6 +1453,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
Settings::values.audio_muted = false;
auto_muted = false;
}
+ UpdateVolumeUI();
}
}
@@ -1446,7 +1467,11 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::RemoveInstalledEntryRequested, this,
&GMainWindow::OnGameListRemoveInstalledEntry);
connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
+ connect(game_list, &GameList::RemovePlayTimeRequested, this,
+ &GMainWindow::OnGameListRemovePlayTimeData);
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
+ connect(game_list, &GameList::VerifyIntegrityRequested, this,
+ &GMainWindow::OnGameListVerifyIntegrity);
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry);
@@ -1537,6 +1562,16 @@ void GMainWindow::ConnectMenuEvents() {
// Tools
connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this,
ReinitializeKeyBehavior::Warning));
+ connect_menu(ui->action_Load_Album, &GMainWindow::OnAlbum);
+ connect_menu(ui->action_Load_Cabinet_Nickname_Owner,
+ [this]() { OnCabinet(Service::NFP::CabinetMode::StartNicknameAndOwnerSettings); });
+ connect_menu(ui->action_Load_Cabinet_Eraser,
+ [this]() { OnCabinet(Service::NFP::CabinetMode::StartGameDataEraser); });
+ connect_menu(ui->action_Load_Cabinet_Restorer,
+ [this]() { OnCabinet(Service::NFP::CabinetMode::StartRestorer); });
+ connect_menu(ui->action_Load_Cabinet_Formatter,
+ [this]() { OnCabinet(Service::NFP::CabinetMode::StartFormatter); });
+ connect_menu(ui->action_Load_Mii_Edit, &GMainWindow::OnMiiEdit);
connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
// TAS
@@ -1547,11 +1582,13 @@ void GMainWindow::ConnectMenuEvents() {
// Help
connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder);
+ connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
connect_menu(ui->action_About, &GMainWindow::OnAbout);
}
void GMainWindow::UpdateMenuState() {
const bool is_paused = emu_thread == nullptr || !emu_thread->IsRunning();
+ const bool is_firmware_available = CheckFirmwarePresence();
const std::array running_actions{
ui->action_Stop,
@@ -1562,10 +1599,23 @@ void GMainWindow::UpdateMenuState() {
ui->action_Pause,
};
+ const std::array applet_actions{
+ ui->action_Load_Album,
+ ui->action_Load_Cabinet_Nickname_Owner,
+ ui->action_Load_Cabinet_Eraser,
+ ui->action_Load_Cabinet_Restorer,
+ ui->action_Load_Cabinet_Formatter,
+ ui->action_Load_Mii_Edit,
+ };
+
for (QAction* action : running_actions) {
action->setEnabled(emulation_running);
}
+ for (QAction* action : applet_actions) {
+ action->setEnabled(is_firmware_available && !emulation_running);
+ }
+
ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused);
if (emulation_running && is_paused) {
@@ -1698,7 +1748,8 @@ void GMainWindow::AllowOSSleep() {
#endif
}
-bool GMainWindow::LoadROM(const QString& filename, u64 program_id, std::size_t program_index) {
+bool GMainWindow::LoadROM(const QString& filename, u64 program_id, std::size_t program_index,
+ AmLaunchType launch_type) {
// Shutdown previous session if the emu thread is still active...
if (emu_thread != nullptr) {
ShutdownGame();
@@ -1710,6 +1761,10 @@ bool GMainWindow::LoadROM(const QString& filename, u64 program_id, std::size_t p
system->SetFilesystem(vfs);
+ if (launch_type == AmLaunchType::UserInitiated) {
+ system->GetUserChannel().clear();
+ }
+
system->SetAppletFrontendSet({
std::make_unique<QtAmiiboSettings>(*this), // Amiibo Settings
(UISettings::values.controller_applet_disabled.GetValue() == true)
@@ -1811,8 +1866,45 @@ bool GMainWindow::SelectAndSetCurrentUser(
return true;
}
+void GMainWindow::ConfigureFilesystemProvider(const std::string& filepath) {
+ // Ensure all NCAs are registered before launching the game
+ const auto file = vfs->OpenFile(filepath, FileSys::Mode::Read);
+ if (!file) {
+ return;
+ }
+
+ auto loader = Loader::GetLoader(*system, file);
+ if (!loader) {
+ return;
+ }
+
+ const auto file_type = loader->GetFileType();
+ if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
+ return;
+ }
+
+ u64 program_id = 0;
+ const auto res2 = loader->ReadProgramId(program_id);
+ if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
+ provider->AddEntry(FileSys::TitleType::Application,
+ FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id,
+ file);
+ } else if (res2 == Loader::ResultStatus::Success &&
+ (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
+ const auto nsp = file_type == Loader::FileType::NSP
+ ? std::make_shared<FileSys::NSP>(file)
+ : FileSys::XCI{file}.GetSecurePartitionNSP();
+ for (const auto& title : nsp->GetNCAs()) {
+ for (const auto& entry : title.second) {
+ provider->AddEntry(entry.first.first, entry.first.second, title.first,
+ entry.second->GetBaseFile());
+ }
+ }
+ }
+}
+
void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t program_index,
- StartGameType type) {
+ StartGameType type, AmLaunchType launch_type) {
LOG_INFO(Frontend, "yuzu starting...");
StoreRecentFile(filename); // Put the filename on top of the list
@@ -1825,6 +1917,7 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
last_filename_booted = filename;
+ ConfigureFilesystemProvider(filename.toStdString());
const auto v_file = Core::GetGameFileFromPath(vfs, filename.toUtf8().constData());
const auto loader = Loader::GetLoader(*system, v_file, program_id, program_index);
@@ -1855,7 +1948,7 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
}
}
- if (!LoadROM(filename, program_id, program_index)) {
+ if (!LoadROM(filename, program_id, program_index, launch_type)) {
return;
}
@@ -1972,8 +2065,16 @@ bool GMainWindow::OnShutdownBegin() {
emit EmulationStopping();
+ int shutdown_time = 1000;
+
+ if (system->DebuggerEnabled()) {
+ shutdown_time = 0;
+ } else if (system->GetExitLocked()) {
+ shutdown_time = 5000;
+ }
+
shutdown_timer.setSingleShot(true);
- shutdown_timer.start(system->DebuggerEnabled() ? 0 : 5000);
+ shutdown_timer.start(shutdown_time);
connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired);
connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped);
@@ -2034,6 +2135,8 @@ void GMainWindow::OnEmulationStopped() {
OnTasStateChanged();
render_window->FinalizeCamera();
+ system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::None);
+
// Enable all controllers
system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
@@ -2229,40 +2332,62 @@ void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) {
QDesktopServices::openUrl(QUrl::fromLocalFile(qt_shader_cache_path));
}
-static std::size_t CalculateRomFSEntrySize(const FileSys::VirtualDir& dir, bool full) {
- std::size_t out = 0;
-
- for (const auto& subdir : dir->GetSubdirectories()) {
- out += 1 + CalculateRomFSEntrySize(subdir, full);
- }
-
- return out + (full ? dir->GetFiles().size() : 0);
-}
-
-static bool RomFSRawCopy(QProgressDialog& dialog, const FileSys::VirtualDir& src,
- const FileSys::VirtualDir& dest, std::size_t block_size, bool full) {
+static bool RomFSRawCopy(size_t total_size, size_t& read_size, QProgressDialog& dialog,
+ const FileSys::VirtualDir& src, const FileSys::VirtualDir& dest,
+ bool full) {
if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
return false;
if (dialog.wasCanceled())
return false;
+ std::vector<u8> buffer(CopyBufferSize);
+ auto last_timestamp = std::chrono::steady_clock::now();
+
+ const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file,
+ const FileSys::VirtualFile& dest_file) {
+ if (src_file == nullptr || dest_file == nullptr) {
+ return false;
+ }
+ if (!dest_file->Resize(src_file->GetSize())) {
+ return false;
+ }
+
+ for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) {
+ if (dialog.wasCanceled()) {
+ dest_file->Resize(0);
+ return false;
+ }
+
+ using namespace std::literals::chrono_literals;
+ const auto new_timestamp = std::chrono::steady_clock::now();
+
+ if ((new_timestamp - last_timestamp) > 33ms) {
+ last_timestamp = new_timestamp;
+ dialog.setValue(
+ static_cast<int>(std::min(read_size, total_size) * 100 / total_size));
+ QCoreApplication::processEvents();
+ }
+
+ const auto read = src_file->Read(buffer.data(), buffer.size(), i);
+ dest_file->Write(buffer.data(), read, i);
+
+ read_size += read;
+ }
+
+ return true;
+ };
+
if (full) {
for (const auto& file : src->GetFiles()) {
const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName());
- if (!FileSys::VfsRawCopy(file, out, block_size))
- return false;
- dialog.setValue(dialog.value() + 1);
- if (dialog.wasCanceled())
+ if (!QtRawCopy(file, out))
return false;
}
}
for (const auto& dir : src->GetSubdirectories()) {
const auto out = dest->CreateSubdirectory(dir->GetName());
- if (!RomFSRawCopy(dialog, dir, out, block_size, full))
- return false;
- dialog.setValue(dialog.value() + 1);
- if (dialog.wasCanceled())
+ if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full))
return false;
}
@@ -2295,9 +2420,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
}
}();
- if (QMessageBox::question(this, tr("Remove Entry"), entry_question,
- QMessageBox::Yes | QMessageBox::No,
- QMessageBox::No) != QMessageBox::Yes) {
+ if (!question(this, tr("Remove Entry"), entry_question, QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No)) {
return;
}
@@ -2396,8 +2520,8 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
}
}();
- if (QMessageBox::question(this, tr("Remove File"), question, QMessageBox::Yes | QMessageBox::No,
- QMessageBox::No) != QMessageBox::Yes) {
+ if (!GMainWindow::question(this, tr("Remove File"), question,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) {
return;
}
@@ -2420,6 +2544,17 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
}
}
+void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
+ if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+
+ play_time_manager->ResetProgramPlayTime(program_id);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+}
+
void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) {
const auto target_file_name = [target] {
switch (target) {
@@ -2535,50 +2670,48 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
return;
}
- FileSys::VirtualFile base_romfs;
- if (loader->ReadRomFS(base_romfs) != Loader::ResultStatus::Success) {
- failed();
- return;
- }
+ FileSys::VirtualFile packed_update_raw{};
+ loader->ReadUpdateRaw(packed_update_raw);
const auto& installed = system->GetContentProvider();
- const auto romfs_title_id = SelectRomFSDumpTarget(installed, program_id);
- if (!romfs_title_id) {
+ u64 title_id{};
+ u8 raw_type{};
+ if (!SelectRomFSDumpTarget(installed, program_id, &title_id, &raw_type)) {
failed();
return;
}
- const auto type = *romfs_title_id == program_id ? FileSys::ContentRecordType::Program
- : FileSys::ContentRecordType::Data;
- const auto base_nca = installed.GetEntry(*romfs_title_id, type);
+ const auto type = static_cast<FileSys::ContentRecordType>(raw_type);
+ const auto base_nca = installed.GetEntry(title_id, type);
if (!base_nca) {
failed();
return;
}
+ const FileSys::NCA update_nca{packed_update_raw, nullptr};
+ if (type != FileSys::ContentRecordType::Program ||
+ update_nca.GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS ||
+ update_nca.GetTitleId() != FileSys::GetUpdateTitleID(title_id)) {
+ packed_update_raw = {};
+ }
+
+ const auto base_romfs = base_nca->GetRomFS();
+ if (!base_romfs) {
+ failed();
+ return;
+ }
+
const auto dump_dir =
target == DumpRomFSTarget::Normal
? Common::FS::GetYuzuPath(Common::FS::YuzuPath::DumpDir)
: Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "atmosphere" / "contents";
- const auto romfs_dir = fmt::format("{:016X}/romfs", *romfs_title_id);
+ const auto romfs_dir = fmt::format("{:016X}/romfs", title_id);
const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir);
- FileSys::VirtualFile romfs;
-
- if (*romfs_title_id == program_id) {
- const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), installed};
- romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, nullptr, false);
- } else {
- romfs = installed.GetEntry(*romfs_title_id, type)->GetRomFS();
- }
-
- const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full);
- if (extracted == nullptr) {
- failed();
- return;
- }
+ const FileSys::PatchManager pm{title_id, system->GetFileSystemController(), installed};
+ auto romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, packed_update_raw, false);
const auto out = VfsFilesystemCreateDirectoryWrapper(vfs, path, FileSys::Mode::ReadWrite);
@@ -2602,11 +2735,16 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
return;
}
+ const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full);
+ if (extracted == nullptr) {
+ failed();
+ return;
+ }
+
const auto full = res == selections.constFirst();
- const auto entry_size = CalculateRomFSEntrySize(extracted, full);
- // The minimum required space is the size of the extracted RomFS + 1 GiB
- const auto minimum_free_space = extracted->GetSize() + 0x40000000;
+ // The expected required space is the size of the RomFS + 1 GiB
+ const auto minimum_free_space = romfs->GetSize() + 0x40000000;
if (full && Common::FS::GetFreeSpaceSize(path) < minimum_free_space) {
QMessageBox::warning(this, tr("RomFS Extraction Failed!"),
@@ -2617,12 +2755,15 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
return;
}
- QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0,
- static_cast<s32>(entry_size), this);
+ QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, 100, this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
+ progress.setAutoClose(false);
+ progress.setAutoReset(false);
- if (RomFSRawCopy(progress, extracted, out, 0x400000, full)) {
+ size_t read_size = 0;
+
+ if (RomFSRawCopy(romfs->GetSize(), read_size, progress, extracted, out, full)) {
progress.close();
QMessageBox::information(this, tr("RomFS Extraction Succeeded!"),
tr("The operation completed successfully."));
@@ -2634,6 +2775,54 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
}
}
+void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) {
+ const auto NotImplemented = [this] {
+ QMessageBox::warning(this, tr("Integrity verification couldn't be performed!"),
+ tr("File contents were not checked for validity."));
+ };
+ const auto Failed = [this] {
+ QMessageBox::critical(this, tr("Integrity verification failed!"),
+ tr("File contents may be corrupt."));
+ };
+
+ const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read));
+ if (loader == nullptr) {
+ NotImplemented();
+ return;
+ }
+
+ QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setMinimumDuration(100);
+ progress.setAutoClose(false);
+ progress.setAutoReset(false);
+
+ const auto QtProgressCallback = [&](size_t processed_size, size_t total_size) {
+ if (progress.wasCanceled()) {
+ return false;
+ }
+
+ progress.setValue(static_cast<int>((processed_size * 100) / total_size));
+ return true;
+ };
+
+ const auto status = loader->VerifyIntegrity(QtProgressCallback);
+ if (progress.wasCanceled() ||
+ status == Loader::ResultStatus::ErrorIntegrityVerificationNotImplemented) {
+ NotImplemented();
+ return;
+ }
+
+ if (status == Loader::ResultStatus::ErrorIntegrityVerificationFailed) {
+ Failed();
+ return;
+ }
+
+ progress.close();
+ QMessageBox::information(this, tr("Integrity verification succeeded!"),
+ tr("The operation completed successfully."));
+}
+
void GMainWindow::OnGameListCopyTID(u64 program_id) {
QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id)));
@@ -2657,7 +2846,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const QStringList args = QApplication::arguments();
std::filesystem::path yuzu_command = args[0].toStdString();
-#if defined(__linux__) || defined(__FreeBSD__)
// If relative path, make it an absolute path
if (yuzu_command.c_str()[0] == '.') {
yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
@@ -2680,44 +2868,52 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
UISettings::values.shortcut_already_warned = true;
}
#endif // __linux__
-#endif // __linux__ || __FreeBSD__
std::filesystem::path target_directory{};
- // Determine target directory for shortcut
-#if defined(__linux__) || defined(__FreeBSD__)
- const char* home = std::getenv("HOME");
- const std::filesystem::path home_path = (home == nullptr ? "~" : home);
- const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
- if (target == GameListShortcutTarget::Desktop) {
- target_directory = home_path / "Desktop";
- if (!Common::FS::IsDir(target_directory)) {
- QMessageBox::critical(
- this, tr("Create Shortcut"),
- tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.")
- .arg(QString::fromStdString(target_directory)),
- QMessageBox::StandardButton::Ok);
- return;
- }
- } else if (target == GameListShortcutTarget::Applications) {
- target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) /
- "applications";
- if (!Common::FS::CreateDirs(target_directory)) {
- QMessageBox::critical(this, tr("Create Shortcut"),
- tr("Cannot create shortcut in applications menu. Path \"%1\" "
- "does not exist and cannot be created.")
- .arg(QString::fromStdString(target_directory)),
- QMessageBox::StandardButton::Ok);
- return;
+ switch (target) {
+ case GameListShortcutTarget::Desktop: {
+ const QString desktop_path =
+ QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+ target_directory = desktop_path.toUtf8().toStdString();
+ break;
+ }
+ case GameListShortcutTarget::Applications: {
+ const QString applications_path =
+ QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
+ if (applications_path.isEmpty()) {
+ const char* home = std::getenv("HOME");
+ if (home != nullptr) {
+ target_directory = std::filesystem::path(home) / ".local/share/applications";
+ }
+ } else {
+ target_directory = applications_path.toUtf8().toStdString();
}
+ break;
+ }
+ default:
+ return;
+ }
+
+ const QDir dir(QString::fromStdString(target_directory.generic_string()));
+ if (!dir.exists()) {
+ QMessageBox::critical(this, tr("Create Shortcut"),
+ tr("Cannot create shortcut. Path \"%1\" does not exist.")
+ .arg(QString::fromStdString(target_directory.generic_string())),
+ QMessageBox::StandardButton::Ok);
+ return;
}
-#endif
const std::string game_file_name = std::filesystem::path(game_path).filename().string();
// Determine full paths for icon and shortcut
#if defined(__linux__) || defined(__FreeBSD__)
+ const char* home = std::getenv("HOME");
+ const std::filesystem::path home_path = (home == nullptr ? "~" : home);
+ const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
+
std::filesystem::path system_icons_path =
- (xdg_data_home == nullptr ? home_path / ".local/share/" : xdg_data_home) /
+ (xdg_data_home == nullptr ? home_path / ".local/share/"
+ : std::filesystem::path(xdg_data_home)) /
"icons/hicolor/256x256";
if (!Common::FS::CreateDirs(system_icons_path)) {
QMessageBox::critical(
@@ -2733,9 +2929,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const std::filesystem::path shortcut_path =
target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name)
: fmt::format("yuzu-{:016X}.desktop", program_id));
+#elif defined(WIN32)
+ std::filesystem::path icons_path =
+ Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir);
+ std::filesystem::path icon_path =
+ icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name)
+ : fmt::format("yuzu-{:016X}.ico", program_id)));
#else
- const std::filesystem::path icon_path{};
- const std::filesystem::path shortcut_path{};
+ std::string icon_extension;
#endif
// Get title from game file
@@ -2760,29 +2961,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
}
- QImage icon_jpeg =
+ QImage icon_data =
QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
#if defined(__linux__) || defined(__FreeBSD__)
// Convert and write the icon as a PNG
- if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) {
+ if (!icon_data.save(QString::fromStdString(icon_path.string()))) {
LOG_ERROR(Frontend, "Could not write icon as PNG to file");
} else {
LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string());
}
+#elif defined(WIN32)
+ if (!SaveIconToFile(icon_path.string(), icon_data)) {
+ LOG_ERROR(Frontend, "Could not write icon to file");
+ return;
+ }
#endif // __linux__
-#if defined(__linux__) || defined(__FreeBSD__)
+#ifdef _WIN32
+ // Replace characters that are illegal in Windows filenames by a dash
+ const std::string illegal_chars = "<>:\"/\\|?*";
+ for (char c : illegal_chars) {
+ std::replace(title.begin(), title.end(), c, '_');
+ }
+ const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str();
+#endif
+
const std::string comment =
tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString();
const std::string arguments = fmt::format("-g \"{:s}\"", game_path);
const std::string categories = "Game;Emulator;Qt;";
const std::string keywords = "Switch;Nintendo;";
-#else
- const std::string comment{};
- const std::string arguments{};
- const std::string categories{};
- const std::string keywords{};
-#endif
+
if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(),
yuzu_command.string(), arguments, categories, keywords)) {
QMessageBox::critical(this, tr("Create Shortcut"),
@@ -2968,10 +3177,9 @@ void GMainWindow::OnMenuInstallToNAND() {
QFuture<InstallResult> future;
InstallResult result;
- if (file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
- file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
+ if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
- future = QtConcurrent::run([this, &file] { return InstallNSPXCI(file); });
+ future = QtConcurrent::run([this, &file] { return InstallNSP(file); });
while (!future.isFinished()) {
QCoreApplication::processEvents();
@@ -3030,7 +3238,7 @@ void GMainWindow::OnMenuInstallToNAND() {
ui->action_Install_File_NAND->setEnabled(true);
}
-InstallResult GMainWindow::InstallNSPXCI(const QString& filename) {
+InstallResult GMainWindow::InstallNSP(const QString& filename) {
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest, std::size_t block_size) {
if (src == nullptr || dest == nullptr) {
@@ -3064,9 +3272,7 @@ InstallResult GMainWindow::InstallNSPXCI(const QString& filename) {
return InstallResult::Failure;
}
} else {
- const auto xci = std::make_shared<FileSys::XCI>(
- vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
- nsp = xci->GetSecurePartitionNSP();
+ return InstallResult::Failure;
}
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
@@ -3192,6 +3398,9 @@ void GMainWindow::OnStartGame() {
UpdateMenuState();
OnTasStateChanged();
+ play_time_manager->SetProgramId(system->GetApplicationProcessProgramID());
+ play_time_manager->Start();
+
discord_rpc->Update();
}
@@ -3199,14 +3408,18 @@ void GMainWindow::OnRestartGame() {
if (!system->IsPoweredOn()) {
return;
}
- // Make a copy since ShutdownGame edits game_path
- const auto current_game = QString(current_game_path);
- ShutdownGame();
- BootGame(current_game);
+
+ if (ConfirmShutdownGame()) {
+ // Make a copy since ShutdownGame edits game_path
+ const auto current_game = QString(current_game_path);
+ ShutdownGame();
+ BootGame(current_game);
+ }
}
void GMainWindow::OnPauseGame() {
emu_thread->SetRunning(false);
+ play_time_manager->Stop();
UpdateMenuState();
AllowOSSleep();
}
@@ -3223,15 +3436,39 @@ void GMainWindow::OnPauseContinueGame() {
}
void GMainWindow::OnStopGame() {
- if (system->GetExitLock() && !ConfirmForceLockedExit()) {
- return;
+ if (ConfirmShutdownGame()) {
+ play_time_manager->Stop();
+ // Update game list to show new play time
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ if (OnShutdownBegin()) {
+ OnShutdownBeginDialog();
+ } else {
+ OnEmulationStopped();
+ }
}
+}
- if (OnShutdownBegin()) {
- OnShutdownBeginDialog();
+bool GMainWindow::ConfirmShutdownGame() {
+ if (UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Always) {
+ if (system->GetExitLocked()) {
+ if (!ConfirmForceLockedExit()) {
+ return false;
+ }
+ } else {
+ if (!ConfirmChangeGame()) {
+ return false;
+ }
+ }
} else {
- OnEmulationStopped();
+ if (UISettings::values.confirm_before_stopping.GetValue() ==
+ ConfirmStop::Ask_Based_On_Game &&
+ system->GetExitLocked()) {
+ if (!ConfirmForceLockedExit()) {
+ return false;
+ }
+ }
}
+ return true;
}
void GMainWindow::OnLoadComplete() {
@@ -3240,7 +3477,8 @@ void GMainWindow::OnLoadComplete() {
void GMainWindow::OnExecuteProgram(std::size_t program_index) {
ShutdownGame();
- BootGame(last_filename_booted, 0, program_index);
+ BootGame(last_filename_booted, 0, program_index, StartGameType::Normal,
+ AmLaunchType::ApplicationInitiated);
}
void GMainWindow::OnExit() {
@@ -3610,22 +3848,11 @@ void GMainWindow::OnTasRecord() {
const bool is_recording = input_subsystem->GetTas()->Record();
if (!is_recording) {
is_tas_recording_dialog_active = true;
- ControllerNavigation* controller_navigation =
- new ControllerNavigation(system->HIDCore(), this);
- // Use QMessageBox instead of question so we can link controller navigation
- QMessageBox* box_dialog = new QMessageBox();
- box_dialog->setWindowTitle(tr("TAS Recording"));
- box_dialog->setText(tr("Overwrite file of player 1?"));
- box_dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
- box_dialog->setDefaultButton(QMessageBox::Yes);
- connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
- [box_dialog](Qt::Key key) {
- QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
- QCoreApplication::postEvent(box_dialog, event);
- });
- int res = box_dialog->exec();
- controller_navigation->UnloadController();
- input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes);
+
+ bool answer = question(this, tr("TAS Recording"), tr("Overwrite file of player 1?"),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ input_subsystem->GetTas()->SaveRecording(answer);
is_tas_recording_dialog_active = false;
}
OnTasStateChanged();
@@ -3636,7 +3863,7 @@ void GMainWindow::OnTasReset() {
}
void GMainWindow::OnToggleDockedMode() {
- const bool is_docked = Settings::values.use_docked_mode.GetValue();
+ const bool is_docked = Settings::IsDockedMode();
auto* player_1 = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
auto* handheld = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
@@ -3650,7 +3877,8 @@ void GMainWindow::OnToggleDockedMode() {
controller_dialog->refreshConfiguration();
}
- Settings::values.use_docked_mode.SetValue(!is_docked);
+ Settings::values.use_docked_mode.SetValue(is_docked ? Settings::ConsoleMode::Handheld
+ : Settings::ConsoleMode::Docked);
UpdateDockedButton();
OnDockedModeChanged(is_docked, !is_docked, *system);
}
@@ -3719,10 +3947,14 @@ void GMainWindow::OnToggleAdaptingFilter() {
void GMainWindow::OnToggleGraphicsAPI() {
auto api = Settings::values.renderer_backend.GetValue();
- if (api == Settings::RendererBackend::OpenGL) {
+ if (api != Settings::RendererBackend::Vulkan) {
api = Settings::RendererBackend::Vulkan;
} else {
+#ifdef HAS_OPENGL
api = Settings::RendererBackend::OpenGL;
+#else
+ api = Settings::RendererBackend::Null;
+#endif
}
Settings::values.renderer_backend.SetValue(api);
renderer_status_button->setChecked(api == Settings::RendererBackend::Vulkan);
@@ -3794,6 +4026,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st
shortcut_stream.close();
return true;
+#elif defined(WIN32)
+ IShellLinkW* shell_link;
+ auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
+ (void**)&shell_link);
+ if (FAILED(hres)) {
+ return false;
+ }
+ shell_link->SetPath(
+ Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to
+ shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data());
+ shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data());
+ shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0);
+
+ IPersistFile* persist_file;
+ hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
+ if (FAILED(hres)) {
+ return false;
+ }
+
+ hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE);
+ if (FAILED(hres)) {
+ return false;
+ }
+
+ persist_file->Release();
+ shell_link->Release();
+
+ return true;
#endif
return false;
}
@@ -3833,6 +4093,29 @@ void GMainWindow::OnLoadAmiibo() {
LoadAmiibo(filename);
}
+bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text,
+ QMessageBox::StandardButtons buttons,
+ QMessageBox::StandardButton defaultButton) {
+
+ QMessageBox* box_dialog = new QMessageBox(parent);
+ box_dialog->setWindowTitle(title);
+ box_dialog->setText(text);
+ box_dialog->setStandardButtons(buttons);
+ box_dialog->setDefaultButton(defaultButton);
+
+ ControllerNavigation* controller_navigation =
+ new ControllerNavigation(system->HIDCore(), box_dialog);
+ connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
+ [box_dialog](Qt::Key key) {
+ QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ QCoreApplication::postEvent(box_dialog, event);
+ });
+ int res = box_dialog->exec();
+
+ controller_navigation->UnloadController();
+ return res == QMessageBox::Yes;
+}
+
void GMainWindow::LoadAmiibo(const QString& filename) {
auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo();
const QString title = tr("Error loading Amiibo data");
@@ -3866,6 +4149,108 @@ void GMainWindow::OnOpenYuzuFolder() {
QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::YuzuDir))));
}
+void GMainWindow::OnVerifyInstalledContents() {
+ // Declare sizes.
+ size_t total_size = 0;
+ size_t processed_size = 0;
+
+ // Initialize a progress dialog.
+ QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setMinimumDuration(100);
+ progress.setAutoClose(false);
+ progress.setAutoReset(false);
+
+ // Declare a list of file names which failed to verify.
+ std::vector<std::string> failed;
+
+ // Declare progress callback.
+ auto QtProgressCallback = [&](size_t nca_processed, size_t nca_total) {
+ if (progress.wasCanceled()) {
+ return false;
+ }
+ progress.setValue(static_cast<int>(((processed_size + nca_processed) * 100) / total_size));
+ return true;
+ };
+
+ // Get content registries.
+ auto bis_contents = system->GetFileSystemController().GetSystemNANDContents();
+ auto user_contents = system->GetFileSystemController().GetUserNANDContents();
+
+ std::vector<FileSys::RegisteredCache*> content_providers;
+ if (bis_contents) {
+ content_providers.push_back(bis_contents);
+ }
+ if (user_contents) {
+ content_providers.push_back(user_contents);
+ }
+
+ // Get associated NCA files.
+ std::vector<FileSys::VirtualFile> nca_files;
+
+ // Get all installed IDs.
+ for (auto nca_provider : content_providers) {
+ const auto entries = nca_provider->ListEntriesFilter();
+
+ for (const auto& entry : entries) {
+ auto nca_file = nca_provider->GetEntryRaw(entry.title_id, entry.type);
+ if (!nca_file) {
+ continue;
+ }
+
+ total_size += nca_file->GetSize();
+ nca_files.push_back(std::move(nca_file));
+ }
+ }
+
+ // Using the NCA loader, determine if all NCAs are valid.
+ for (auto& nca_file : nca_files) {
+ Loader::AppLoader_NCA nca_loader(nca_file);
+
+ auto status = nca_loader.VerifyIntegrity(QtProgressCallback);
+ if (progress.wasCanceled()) {
+ break;
+ }
+ if (status != Loader::ResultStatus::Success) {
+ FileSys::NCA nca(nca_file);
+ const auto title_id = nca.GetTitleId();
+ std::string title_name = "unknown";
+
+ const auto control = provider->GetEntry(FileSys::GetBaseTitleID(title_id),
+ FileSys::ContentRecordType::Control);
+ if (control && control->GetStatus() == Loader::ResultStatus::Success) {
+ const FileSys::PatchManager pm{title_id, system->GetFileSystemController(),
+ *provider};
+ const auto [nacp, logo] = pm.ParseControlNCA(*control);
+ if (nacp) {
+ title_name = nacp->GetApplicationName();
+ }
+ }
+
+ if (title_id > 0) {
+ failed.push_back(
+ fmt::format("{} ({:016X}) ({})", nca_file->GetName(), title_id, title_name));
+ } else {
+ failed.push_back(fmt::format("{} (unknown)", nca_file->GetName()));
+ }
+ }
+
+ processed_size += nca_file->GetSize();
+ }
+
+ progress.close();
+
+ if (failed.size() > 0) {
+ auto failed_names = QString::fromStdString(fmt::format("{}", fmt::join(failed, "\n")));
+ QMessageBox::critical(
+ this, tr("Integrity verification failed!"),
+ tr("Verification failed for the following files:\n\n%1").arg(failed_names));
+ } else {
+ QMessageBox::information(this, tr("Integrity verification succeeded!"),
+ tr("The operation completed successfully."));
+ }
+}
+
void GMainWindow::OnAbout() {
AboutDialog aboutDialog(this);
aboutDialog.exec();
@@ -3884,6 +4269,76 @@ void GMainWindow::OnToggleStatusBar() {
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
}
+void GMainWindow::OnAlbum() {
+ constexpr u64 AlbumId = 0x010000000000100Dull;
+ auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
+ if (!bis_system) {
+ QMessageBox::warning(this, tr("No firmware available"),
+ tr("Please install the firmware to use the Album applet."));
+ return;
+ }
+
+ auto album_nca = bis_system->GetEntry(AlbumId, FileSys::ContentRecordType::Program);
+ if (!album_nca) {
+ QMessageBox::warning(this, tr("Album Applet"),
+ tr("Album applet is not available. Please reinstall firmware."));
+ return;
+ }
+
+ system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::PhotoViewer);
+
+ const auto filename = QString::fromStdString(album_nca->GetFullPath());
+ UISettings::values.roms_path = QFileInfo(filename).path();
+ BootGame(filename);
+}
+
+void GMainWindow::OnCabinet(Service::NFP::CabinetMode mode) {
+ constexpr u64 CabinetId = 0x0100000000001002ull;
+ auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
+ if (!bis_system) {
+ QMessageBox::warning(this, tr("No firmware available"),
+ tr("Please install the firmware to use the Cabinet applet."));
+ return;
+ }
+
+ auto cabinet_nca = bis_system->GetEntry(CabinetId, FileSys::ContentRecordType::Program);
+ if (!cabinet_nca) {
+ QMessageBox::warning(this, tr("Cabinet Applet"),
+ tr("Cabinet applet is not available. Please reinstall firmware."));
+ return;
+ }
+
+ system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::Cabinet);
+ system->GetAppletManager().SetCabinetMode(mode);
+
+ const auto filename = QString::fromStdString(cabinet_nca->GetFullPath());
+ UISettings::values.roms_path = QFileInfo(filename).path();
+ BootGame(filename);
+}
+
+void GMainWindow::OnMiiEdit() {
+ constexpr u64 MiiEditId = 0x0100000000001009ull;
+ auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
+ if (!bis_system) {
+ QMessageBox::warning(this, tr("No firmware available"),
+ tr("Please install the firmware to use the Mii editor."));
+ return;
+ }
+
+ auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
+ if (!mii_applet_nca) {
+ QMessageBox::warning(this, tr("Mii Edit Applet"),
+ tr("Mii editor is not available. Please reinstall firmware."));
+ return;
+ }
+
+ system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::MiiEdit);
+
+ const auto filename = QString::fromStdString((mii_applet_nca->GetFullPath()));
+ UISettings::values.roms_path = QFileInfo(filename).path();
+ BootGame(filename);
+}
+
void GMainWindow::OnCaptureScreenshot() {
if (emu_thread == nullptr || !emu_thread->IsRunning()) {
return;
@@ -4080,10 +4535,10 @@ void GMainWindow::UpdateGPUAccuracyButton() {
}
void GMainWindow::UpdateDockedButton() {
- const bool is_docked = Settings::values.use_docked_mode.GetValue();
- dock_status_button->setChecked(is_docked);
+ const auto console_mode = Settings::values.use_docked_mode.GetValue();
+ dock_status_button->setChecked(Settings::IsDockedMode());
dock_status_button->setText(
- Config::use_docked_mode_texts_map.find(is_docked)->second.toUpper());
+ Config::use_docked_mode_texts_map.find(console_mode)->second.toUpper());
}
void GMainWindow::UpdateAPIText() {
@@ -4290,6 +4745,8 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
if (behavior == ReinitializeKeyBehavior::Warning) {
game_list->PopulateAsync(UISettings::values.game_dirs);
}
+
+ UpdateMenuState();
}
bool GMainWindow::CheckSystemArchiveDecryption() {
@@ -4311,28 +4768,63 @@ bool GMainWindow::CheckSystemArchiveDecryption() {
return mii_nca->GetRomFS().get() != nullptr;
}
-std::optional<u64> GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed,
- u64 program_id) {
- const auto dlc_entries =
- installed.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
- std::vector<FileSys::ContentProviderEntry> dlc_match;
- dlc_match.reserve(dlc_entries.size());
- std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
- [&program_id, &installed](const FileSys::ContentProviderEntry& entry) {
- return FileSys::GetBaseTitleID(entry.title_id) == program_id &&
- installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success;
- });
-
- std::vector<u64> romfs_tids;
- romfs_tids.push_back(program_id);
- for (const auto& entry : dlc_match) {
- romfs_tids.push_back(entry.title_id);
- }
-
- if (romfs_tids.size() > 1) {
- QStringList list{QStringLiteral("Base")};
- for (std::size_t i = 1; i < romfs_tids.size(); ++i) {
- list.push_back(QStringLiteral("DLC %1").arg(romfs_tids[i] & 0x7FF));
+bool GMainWindow::CheckFirmwarePresence() {
+ constexpr u64 MiiEditId = 0x0100000000001009ull;
+
+ auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
+ if (!bis_system) {
+ return false;
+ }
+
+ auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
+ if (!mii_applet_nca) {
+ return false;
+ }
+
+ return true;
+}
+
+bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id,
+ u64* selected_title_id, u8* selected_content_record_type) {
+ using ContentInfo = std::tuple<u64, FileSys::TitleType, FileSys::ContentRecordType>;
+ boost::container::flat_set<ContentInfo> available_title_ids;
+
+ const auto RetrieveEntries = [&](FileSys::TitleType title_type,
+ FileSys::ContentRecordType record_type) {
+ const auto entries = installed.ListEntriesFilter(title_type, record_type);
+ for (const auto& entry : entries) {
+ if (FileSys::GetBaseTitleID(entry.title_id) == program_id &&
+ installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) {
+ available_title_ids.insert({entry.title_id, title_type, record_type});
+ }
+ }
+ };
+
+ RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program);
+ RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument);
+ RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation);
+ RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
+
+ if (available_title_ids.empty()) {
+ return false;
+ }
+
+ size_t title_index = 0;
+
+ if (available_title_ids.size() > 1) {
+ QStringList list;
+ for (auto& [title_id, title_type, record_type] : available_title_ids) {
+ const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id));
+ if (record_type == FileSys::ContentRecordType::Program) {
+ list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id));
+ } else if (record_type == FileSys::ContentRecordType::HtmlDocument) {
+ list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id));
+ } else if (record_type == FileSys::ContentRecordType::LegalInformation) {
+ list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id));
+ } else {
+ list.push_back(
+ QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id));
+ }
}
bool ok;
@@ -4340,13 +4832,16 @@ std::optional<u64> GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProv
this, tr("Select RomFS Dump Target"),
tr("Please select which RomFS you would like to dump."), list, 0, false, &ok);
if (!ok) {
- return {};
+ return false;
}
- return romfs_tids[list.indexOf(res)];
+ title_index = list.indexOf(res);
}
- return program_id;
+ const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index);
+ *selected_title_id = title_id;
+ *selected_content_record_type = static_cast<u8>(record_type);
+ return true;
}
bool GMainWindow::ConfirmClose() {
@@ -4354,8 +4849,7 @@ bool GMainWindow::ConfirmClose() {
return true;
}
const auto text = tr("Are you sure you want to close yuzu?");
- const auto answer = QMessageBox::question(this, tr("yuzu"), text);
- return answer != QMessageBox::No;
+ return question(this, tr("yuzu"), text);
}
void GMainWindow::closeEvent(QCloseEvent* event) {
@@ -4448,11 +4942,11 @@ bool GMainWindow::ConfirmChangeGame() {
if (emu_thread == nullptr)
return true;
- const auto answer = QMessageBox::question(
+ // Use custom question to link controller navigation
+ return question(
this, tr("yuzu"),
tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."),
- QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
- return answer != QMessageBox::No;
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
}
bool GMainWindow::ConfirmForceLockedExit() {
@@ -4462,8 +4956,7 @@ bool GMainWindow::ConfirmForceLockedExit() {
const auto text = tr("The currently running application has requested yuzu to not exit.\n\n"
"Would you like to bypass this and exit anyway?");
- const auto answer = QMessageBox::question(this, tr("yuzu"), text);
- return answer != QMessageBox::No;
+ return question(this, tr("yuzu"), text);
}
void GMainWindow::RequestGameExit() {
@@ -4476,6 +4969,8 @@ void GMainWindow::RequestGameExit() {
auto applet_ae = sm.GetService<Service::AM::AppletAE>("appletAE");
bool has_signalled = false;
+ system->SetExitRequested(true);
+
if (applet_oe != nullptr) {
applet_oe->GetMessageQueue()->RequestExit();
has_signalled = true;
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 2cfb96257..270a40c5f 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -7,6 +7,7 @@
#include <optional>
#include <QMainWindow>
+#include <QMessageBox>
#include <QTimer>
#include <QTranslator>
@@ -15,6 +16,7 @@
#include "input_common/drivers/tas_input.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/hotkeys.h"
+#include "yuzu/util/controller_navigation.h"
#ifdef __unix__
#include <QVariant>
@@ -58,6 +60,11 @@ enum class StartGameType {
Global, // Only uses global configuration
};
+enum class AmLaunchType {
+ UserInitiated,
+ ApplicationInitiated,
+};
+
namespace Core {
enum class SystemResultStatus : u32;
class System;
@@ -76,6 +83,10 @@ namespace DiscordRPC {
class DiscordInterface;
}
+namespace PlayTime {
+class PlayTimeManager;
+}
+
namespace FileSys {
class ContentProvider;
class ManualContentProvider;
@@ -97,6 +108,10 @@ namespace Service::NFC {
class NfcDevice;
} // namespace Service::NFC
+namespace Service::NFP {
+enum class CabinetMode : u8;
+} // namespace Service::NFP
+
namespace Ui {
class MainWindow;
}
@@ -239,9 +254,11 @@ private:
void PreventOSSleep();
void AllowOSSleep();
- bool LoadROM(const QString& filename, u64 program_id, std::size_t program_index);
+ bool LoadROM(const QString& filename, u64 program_id, std::size_t program_index,
+ AmLaunchType launch_type);
void BootGame(const QString& filename, u64 program_id = 0, std::size_t program_index = 0,
- StartGameType with_config = StartGameType::Normal);
+ StartGameType with_config = StartGameType::Normal,
+ AmLaunchType launch_type = AmLaunchType::UserInitiated);
void ShutdownGame();
void ShowTelemetryCallout();
@@ -312,7 +329,9 @@ private slots:
void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type);
void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target,
const std::string& game_path);
+ void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
+ void OnGameListVerifyIntegrity(const std::string& game_path);
void OnGameListCopyTID(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list);
@@ -342,6 +361,7 @@ private slots:
void OnConfigurePerGame();
void OnLoadAmiibo();
void OnOpenYuzuFolder();
+ void OnVerifyInstalledContents();
void OnAbout();
void OnToggleFilterBar();
void OnToggleStatusBar();
@@ -356,6 +376,9 @@ private slots:
void ResetWindowSize720();
void ResetWindowSize900();
void ResetWindowSize1080();
+ void OnAlbum();
+ void OnCabinet(Service::NFP::CabinetMode mode);
+ void OnMiiEdit();
void OnCaptureScreenshot();
void OnReinitializeKeys(ReinitializeKeyBehavior behavior);
void OnLanguageChanged(const QString& locale);
@@ -374,9 +397,11 @@ private:
void RemoveVulkanDriverPipelineCache(u64 program_id);
void RemoveAllTransferableShaderCaches(u64 program_id);
void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
+ void RemovePlayTimeData(u64 program_id);
void RemoveCacheStorage(u64 program_id);
- std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id);
- InstallResult InstallNSPXCI(const QString& filename);
+ bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
+ u64* selected_title_id, u8* selected_content_record_type);
+ InstallResult InstallNSP(const QString& filename);
InstallResult InstallNCA(const QString& filename);
void MigrateConfigFiles();
void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
@@ -399,6 +424,13 @@ private:
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
bool CheckDarkMode();
bool CheckSystemArchiveDecryption();
+ bool CheckFirmwarePresence();
+ void ConfigureFilesystemProvider(const std::string& filepath);
+ /**
+ * Open (or not) the right confirm dialog based on current setting and game exit lock
+ * @returns true if the player confirmed or the settings do no require it
+ */
+ bool ConfirmShutdownGame();
QString GetTasStateDescription() const;
bool CreateShortcut(const std::string& shortcut_path, const std::string& title,
@@ -406,10 +438,22 @@ private:
const std::string& command, const std::string& arguments,
const std::string& categories, const std::string& keywords);
+ /**
+ * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog
+ * The only difference is that it returns a boolean.
+ *
+ * @returns true if buttons contains QMessageBox::Yes and the user clicks on the "Yes" button.
+ */
+ bool question(QWidget* parent, const QString& title, const QString& text,
+ QMessageBox::StandardButtons buttons =
+ QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No),
+ QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
+
std::unique_ptr<Ui::MainWindow> ui;
std::unique_ptr<Core::System> system;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
+ std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
MultiplayerState* multiplayer_state = nullptr;
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index 013ba0ceb..88684ffb5 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -137,6 +137,15 @@
<property name="title">
<string>&amp;Tools</string>
</property>
+ <widget class="QMenu" name="menu_cabinet_applet">
+ <property name="title">
+ <string>&amp;Amiibo</string>
+ </property>
+ <addaction name="action_Load_Cabinet_Nickname_Owner"/>
+ <addaction name="action_Load_Cabinet_Eraser"/>
+ <addaction name="action_Load_Cabinet_Restorer"/>
+ <addaction name="action_Load_Cabinet_Formatter"/>
+ </widget>
<widget class="QMenu" name="menuTAS">
<property name="title">
<string>&amp;TAS</string>
@@ -148,6 +157,11 @@
<addaction name="action_Configure_Tas"/>
</widget>
<addaction name="action_Rederive"/>
+ <addaction name="action_Verify_installed_contents"/>
+ <addaction name="separator"/>
+ <addaction name="menu_cabinet_applet"/>
+ <addaction name="action_Load_Album"/>
+ <addaction name="action_Load_Mii_Edit"/>
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
<addaction name="menuTAS"/>
@@ -214,6 +228,11 @@
<string>&amp;Reinitialize keys...</string>
</property>
</action>
+ <action name="action_Verify_installed_contents">
+ <property name="text">
+ <string>&amp;Verify Installed Contents</string>
+ </property>
+ </action>
<action name="action_About">
<property name="text">
<string>&amp;About yuzu</string>
@@ -362,6 +381,36 @@
<string>&amp;Capture Screenshot</string>
</property>
</action>
+ <action name="action_Load_Album">
+ <property name="text">
+ <string>Open &amp;Album</string>
+ </property>
+ </action>
+ <action name="action_Load_Cabinet_Nickname_Owner">
+ <property name="text">
+ <string>&amp;Set Nickname and Owner</string>
+ </property>
+ </action>
+ <action name="action_Load_Cabinet_Eraser">
+ <property name="text">
+ <string>&amp;Delete Game Data</string>
+ </property>
+ </action>
+ <action name="action_Load_Cabinet_Restorer">
+ <property name="text">
+ <string>&amp;Restore Amiibo</string>
+ </property>
+ </action>
+ <action name="action_Load_Cabinet_Formatter">
+ <property name="text">
+ <string>&amp;Format Amiibo</string>
+ </property>
+ </action>
+ <action name="action_Load_Mii_Edit">
+ <property name="text">
+ <string>Open &amp;Mii Editor</string>
+ </property>
+ </action>
<action name="action_Configure_Tas">
<property name="text">
<string>&amp;Configure TAS...</string>
diff --git a/src/yuzu/play_time_manager.cpp b/src/yuzu/play_time_manager.cpp
new file mode 100644
index 000000000..155c36b7d
--- /dev/null
+++ b/src/yuzu/play_time_manager.cpp
@@ -0,0 +1,179 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/alignment.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "core/hle/service/acc/profile_manager.h"
+#include "yuzu/play_time_manager.h"
+
+namespace PlayTime {
+
+namespace {
+
+struct PlayTimeElement {
+ ProgramId program_id;
+ PlayTime play_time;
+};
+
+std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() {
+ const Service::Account::ProfileManager manager;
+ const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
+ if (!uuid.has_value()) {
+ return std::nullopt;
+ }
+ return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) /
+ uuid->RawString().append(".bin");
+}
+
+[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) {
+ const auto filename = GetCurrentUserPlayTimePath();
+
+ if (!filename.has_value()) {
+ LOG_ERROR(Frontend, "Failed to get current user path");
+ return false;
+ }
+
+ out_play_time_db.clear();
+
+ if (Common::FS::Exists(filename.value())) {
+ Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read,
+ Common::FS::FileType::BinaryFile};
+ if (!file.IsOpen()) {
+ LOG_ERROR(Frontend, "Failed to open play time file: {}",
+ Common::FS::PathToUTF8String(filename.value()));
+ return false;
+ }
+
+ const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement);
+ std::vector<PlayTimeElement> elements(num_elements);
+
+ if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) {
+ return false;
+ }
+
+ for (const auto& [program_id, play_time] : elements) {
+ if (program_id != 0) {
+ out_play_time_db[program_id] = play_time;
+ }
+ }
+ }
+
+ return true;
+}
+
+[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) {
+ const auto filename = GetCurrentUserPlayTimePath();
+
+ if (!filename.has_value()) {
+ LOG_ERROR(Frontend, "Failed to get current user path");
+ return false;
+ }
+
+ Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write,
+ Common::FS::FileType::BinaryFile};
+ if (!file.IsOpen()) {
+ LOG_ERROR(Frontend, "Failed to open play time file: {}",
+ Common::FS::PathToUTF8String(filename.value()));
+ return false;
+ }
+
+ std::vector<PlayTimeElement> elements;
+ elements.reserve(play_time_db.size());
+
+ for (auto& [program_id, play_time] : play_time_db) {
+ if (program_id != 0) {
+ elements.push_back(PlayTimeElement{program_id, play_time});
+ }
+ }
+
+ return file.WriteSpan<PlayTimeElement>(elements) == elements.size();
+}
+
+} // namespace
+
+PlayTimeManager::PlayTimeManager() {
+ if (!ReadPlayTimeFile(database)) {
+ LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
+ }
+}
+
+PlayTimeManager::~PlayTimeManager() {
+ Save();
+}
+
+void PlayTimeManager::SetProgramId(u64 program_id) {
+ running_program_id = program_id;
+}
+
+void PlayTimeManager::Start() {
+ play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); });
+}
+
+void PlayTimeManager::Stop() {
+ play_time_thread = {};
+}
+
+void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
+ Common::SetCurrentThreadName("PlayTimeReport");
+
+ using namespace std::literals::chrono_literals;
+ using std::chrono::seconds;
+ using std::chrono::steady_clock;
+
+ auto timestamp = steady_clock::now();
+
+ const auto GetDuration = [&]() -> u64 {
+ const auto last_timestamp = std::exchange(timestamp, steady_clock::now());
+ const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp);
+ return static_cast<u64>(duration.count());
+ };
+
+ while (!stop_token.stop_requested()) {
+ Common::StoppableTimedWait(stop_token, 30s);
+
+ database[running_program_id] += GetDuration();
+ Save();
+ }
+}
+
+void PlayTimeManager::Save() {
+ if (!WritePlayTimeFile(database)) {
+ LOG_ERROR(Frontend, "Failed to update play time database!");
+ }
+}
+
+u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
+ auto it = database.find(program_id);
+ if (it != database.end()) {
+ return it->second;
+ } else {
+ return 0;
+ }
+}
+
+void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
+ database.erase(program_id);
+ Save();
+}
+
+QString ReadablePlayTime(qulonglong time_seconds) {
+ if (time_seconds == 0) {
+ return {};
+ }
+ const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0);
+ const auto time_hours = static_cast<double>(time_seconds) / 3600;
+ const bool is_minutes = time_minutes < 60;
+ const char* unit = is_minutes ? "m" : "h";
+ const auto value = is_minutes ? time_minutes : time_hours;
+
+ return QStringLiteral("%L1 %2")
+ .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0)
+ .arg(QString::fromUtf8(unit));
+}
+
+} // namespace PlayTime
diff --git a/src/yuzu/play_time_manager.h b/src/yuzu/play_time_manager.h
new file mode 100644
index 000000000..5f96f3447
--- /dev/null
+++ b/src/yuzu/play_time_manager.h
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QString>
+
+#include <map>
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+#include "common/polyfill_thread.h"
+
+namespace PlayTime {
+
+using ProgramId = u64;
+using PlayTime = u64;
+using PlayTimeDatabase = std::map<ProgramId, PlayTime>;
+
+class PlayTimeManager {
+public:
+ explicit PlayTimeManager();
+ ~PlayTimeManager();
+
+ YUZU_NON_COPYABLE(PlayTimeManager);
+ YUZU_NON_MOVEABLE(PlayTimeManager);
+
+ u64 GetPlayTime(u64 program_id) const;
+ void ResetProgramPlayTime(u64 program_id);
+ void SetProgramId(u64 program_id);
+ void Start();
+ void Stop();
+
+private:
+ PlayTimeDatabase database;
+ u64 running_program_id;
+ std::jthread play_time_thread;
+ void AutoTimestamp(std::stop_token stop_token);
+ void Save();
+};
+
+QString ReadablePlayTime(qulonglong time_seconds);
+
+} // namespace PlayTime
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index 8efd63f31..b62ff620c 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -16,7 +16,9 @@
#include "common/settings_enums.h"
using Settings::Category;
+using Settings::ConfirmStop;
using Settings::Setting;
+using Settings::SwitchableSetting;
#ifndef CANNOT_EXPLICITLY_INSTANTIATE
namespace Settings {
@@ -94,6 +96,15 @@ struct Values {
Setting<bool> confirm_before_closing{
linkage, true, "confirmClose", Category::UiGeneral, Settings::Specialization::Default,
true, true};
+
+ SwitchableSetting<ConfirmStop> confirm_before_stopping{linkage,
+ ConfirmStop::Ask_Always,
+ "confirmStop",
+ Category::UiGeneral,
+ Settings::Specialization::Default,
+ true,
+ true};
+
Setting<bool> first_start{linkage, true, "firstStart", Category::Ui};
Setting<bool> pause_when_in_background{linkage,
false,
@@ -103,7 +114,7 @@ struct Values {
true,
true};
Setting<bool> mute_when_in_background{
- linkage, false, "muteWhenInBackground", Category::Ui, Settings::Specialization::Default,
+ linkage, false, "muteWhenInBackground", Category::Audio, Settings::Specialization::Default,
true, true};
Setting<bool> hide_mouse{
linkage, true, "hideInactiveMouse", Category::UiGeneral, Settings::Specialization::Default,
@@ -183,6 +194,9 @@ struct Values {
Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList};
Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList};
+ // Play time
+ Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList};
+
bool configuration_applied;
bool reset_to_defaults;
bool shortcut_already_warned{false};
diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp
index 5c3e4589e..f2854c8ec 100644
--- a/src/yuzu/util/util.cpp
+++ b/src/yuzu/util/util.cpp
@@ -5,6 +5,10 @@
#include <cmath>
#include <QPainter>
#include "yuzu/util/util.h"
+#ifdef _WIN32
+#include <windows.h>
+#include "common/fs/file.h"
+#endif
QFont GetMonospaceFont() {
QFont font(QStringLiteral("monospace"));
@@ -37,3 +41,101 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) {
painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0);
return circle_pixmap;
}
+
+bool SaveIconToFile(const std::string_view path, const QImage& image) {
+#if defined(WIN32)
+#pragma pack(push, 2)
+ struct IconDir {
+ WORD id_reserved;
+ WORD id_type;
+ WORD id_count;
+ };
+
+ struct IconDirEntry {
+ BYTE width;
+ BYTE height;
+ BYTE color_count;
+ BYTE reserved;
+ WORD planes;
+ WORD bit_count;
+ DWORD bytes_in_res;
+ DWORD image_offset;
+ };
+#pragma pack(pop)
+
+ const QImage source_image = image.convertToFormat(QImage::Format_RGB32);
+ constexpr std::array<int, 7> scale_sizes{256, 128, 64, 48, 32, 24, 16};
+ constexpr int bytes_per_pixel = 4;
+
+ const IconDir icon_dir{
+ .id_reserved = 0,
+ .id_type = 1,
+ .id_count = static_cast<WORD>(scale_sizes.size()),
+ };
+
+ Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write,
+ Common::FS::FileType::BinaryFile);
+ if (!icon_file.IsOpen()) {
+ return false;
+ }
+
+ if (!icon_file.Write(icon_dir)) {
+ return false;
+ }
+
+ std::size_t image_offset = sizeof(IconDir) + (sizeof(IconDirEntry) * scale_sizes.size());
+ for (std::size_t i = 0; i < scale_sizes.size(); i++) {
+ const int image_size = scale_sizes[i] * scale_sizes[i] * bytes_per_pixel;
+ const IconDirEntry icon_entry{
+ .width = static_cast<BYTE>(scale_sizes[i]),
+ .height = static_cast<BYTE>(scale_sizes[i]),
+ .color_count = 0,
+ .reserved = 0,
+ .planes = 1,
+ .bit_count = bytes_per_pixel * 8,
+ .bytes_in_res = static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size),
+ .image_offset = static_cast<DWORD>(image_offset),
+ };
+ image_offset += icon_entry.bytes_in_res;
+ if (!icon_file.Write(icon_entry)) {
+ return false;
+ }
+ }
+
+ for (std::size_t i = 0; i < scale_sizes.size(); i++) {
+ const QImage scaled_image = source_image.scaled(
+ scale_sizes[i], scale_sizes[i], Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ const BITMAPINFOHEADER info_header{
+ .biSize = sizeof(BITMAPINFOHEADER),
+ .biWidth = scaled_image.width(),
+ .biHeight = scaled_image.height() * 2,
+ .biPlanes = 1,
+ .biBitCount = bytes_per_pixel * 8,
+ .biCompression = BI_RGB,
+ .biSizeImage{},
+ .biXPelsPerMeter{},
+ .biYPelsPerMeter{},
+ .biClrUsed{},
+ .biClrImportant{},
+ };
+
+ if (!icon_file.Write(info_header)) {
+ return false;
+ }
+
+ for (int y = 0; y < scaled_image.height(); y++) {
+ const auto* line = scaled_image.scanLine(scaled_image.height() - 1 - y);
+ std::vector<u8> line_data(scaled_image.width() * bytes_per_pixel);
+ std::memcpy(line_data.data(), line, line_data.size());
+ if (!icon_file.Write(line_data)) {
+ return false;
+ }
+ }
+ }
+ icon_file.Close();
+
+ return true;
+#else
+ return false;
+#endif
+}
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h
index 39dd2d895..09c14ce3f 100644
--- a/src/yuzu/util/util.h
+++ b/src/yuzu/util/util.h
@@ -7,14 +7,22 @@
#include <QString>
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
-QFont GetMonospaceFont();
+[[nodiscard]] QFont GetMonospaceFont();
/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
-QString ReadableByteSize(qulonglong size);
+[[nodiscard]] QString ReadableByteSize(qulonglong size);
/**
* Creates a circle pixmap from a specified color
* @param color The color the pixmap shall have
* @return QPixmap circle pixmap
*/
-QPixmap CreateCirclePixmapFromColor(const QColor& color);
+[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
+
+/**
+ * Saves a windows icon to a file
+ * @param path The icons path
+ * @param image The image to save
+ * @return bool If the operation succeeded
+ */
+[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image);