summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Mario2023-08-26 21:19:00 -0400
committerGravatar Liam2023-08-26 22:20:19 -0400
commit5464423667dedc0f09d48f85fc7871a3e56127a4 (patch)
treea981bb0c3f41050a476e791f4440bc4eae9e1ebf /src
parentMerge pull request #11356 from lat9nq/console-mode-pg (diff)
downloadyuzu-5464423667dedc0f09d48f85fc7871a3e56127a4.tar.gz
yuzu-5464423667dedc0f09d48f85fc7871a3e56127a4.tar.xz
yuzu-5464423667dedc0f09d48f85fc7871a3e56127a4.zip
yuzu-qt: Track play time
Diffstat (limited to 'src')
-rw-r--r--src/common/fs/fs_paths.h1
-rw-r--r--src/common/fs/path_util.cpp1
-rw-r--r--src/common/fs/path_util.h1
-rw-r--r--src/yuzu/CMakeLists.txt2
-rw-r--r--src/yuzu/configuration/configure_ui.cpp5
-rw-r--r--src/yuzu/configuration/configure_ui.ui7
-rw-r--r--src/yuzu/game_list.cpp6
-rw-r--r--src/yuzu/game_list.h2
-rw-r--r--src/yuzu/game_list_p.h26
-rw-r--r--src/yuzu/game_list_worker.cpp2
-rw-r--r--src/yuzu/main.cpp26
-rw-r--r--src/yuzu/main.h7
-rw-r--r--src/yuzu/play_time.cpp177
-rw-r--r--src/yuzu/play_time.h68
-rw-r--r--src/yuzu/uisettings.h3
15 files changed, 334 insertions, 0 deletions
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h
index 61bac9eba..59d66f71e 100644
--- a/src/common/fs/fs_paths.h
+++ b/src/common/fs/fs_paths.h
@@ -18,6 +18,7 @@
18#define LOAD_DIR "load" 18#define LOAD_DIR "load"
19#define LOG_DIR "log" 19#define LOG_DIR "log"
20#define NAND_DIR "nand" 20#define NAND_DIR "nand"
21#define PLAY_TIME_DIR "play_time"
21#define SCREENSHOTS_DIR "screenshots" 22#define SCREENSHOTS_DIR "screenshots"
22#define SDMC_DIR "sdmc" 23#define SDMC_DIR "sdmc"
23#define SHADER_DIR "shader" 24#define SHADER_DIR "shader"
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index d71cfacc6..3bbe9ff87 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -122,6 +122,7 @@ public:
122 GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); 122 GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR);
123 GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); 123 GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR);
124 GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR); 124 GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR);
125 GenerateYuzuPath(YuzuPath::PlayTimeDir, yuzu_path / PLAY_TIME_DIR);
125 GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); 126 GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
126 GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); 127 GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
127 GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); 128 GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index ba28964d0..289974e32 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -20,6 +20,7 @@ enum class YuzuPath {
20 LoadDir, // Where cheat/mod files are stored. 20 LoadDir, // Where cheat/mod files are stored.
21 LogDir, // Where log files are stored. 21 LogDir, // Where log files are stored.
22 NANDDir, // Where the emulated NAND is stored. 22 NANDDir, // Where the emulated NAND is stored.
23 PlayTimeDir, // Where play time data is stored.
23 ScreenshotsDir, // Where yuzu screenshots are stored. 24 ScreenshotsDir, // Where yuzu screenshots are stored.
24 SDMCDir, // Where the emulated SDMC is stored. 25 SDMCDir, // Where the emulated SDMC is stored.
25 ShaderDir, // Where shaders are stored. 26 ShaderDir, // Where shaders are stored.
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 8f86a1553..89763f64f 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -195,6 +195,8 @@ add_executable(yuzu
195 multiplayer/state.cpp 195 multiplayer/state.cpp
196 multiplayer/state.h 196 multiplayer/state.h
197 multiplayer/validation.h 197 multiplayer/validation.h
198 play_time.cpp
199 play_time.h
198 precompiled_headers.h 200 precompiled_headers.h
199 qt_common.cpp 201 qt_common.cpp
200 qt_common.h 202 qt_common.h
diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp
index 34ab01617..058bd9f4d 100644
--- a/src/yuzu/configuration/configure_ui.cpp
+++ b/src/yuzu/configuration/configure_ui.cpp
@@ -126,6 +126,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
126 connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); 126 connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
127 connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); 127 connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
128 connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); 128 connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
129 connect(ui->show_play_time, &QCheckBox::stateChanged, this,
130 &ConfigureUi::RequestGameListUpdate);
129 connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, 131 connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
130 &ConfigureUi::RequestGameListUpdate); 132 &ConfigureUi::RequestGameListUpdate);
131 connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), 133 connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged),
@@ -170,6 +172,7 @@ void ConfigureUi::ApplyConfiguration() {
170 UISettings::values.show_compat = ui->show_compat->isChecked(); 172 UISettings::values.show_compat = ui->show_compat->isChecked();
171 UISettings::values.show_size = ui->show_size->isChecked(); 173 UISettings::values.show_size = ui->show_size->isChecked();
172 UISettings::values.show_types = ui->show_types->isChecked(); 174 UISettings::values.show_types = ui->show_types->isChecked();
175 UISettings::values.show_play_time = ui->show_play_time->isChecked();
173 UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt(); 176 UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt();
174 UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt(); 177 UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt();
175 UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); 178 UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt();
@@ -182,6 +185,7 @@ void ConfigureUi::ApplyConfiguration() {
182 const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); 185 const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText());
183 UISettings::values.screenshot_height.SetValue(height); 186 UISettings::values.screenshot_height.SetValue(height);
184 187
188 RequestGameListUpdate();
185 system.ApplySettings(); 189 system.ApplySettings();
186} 190}
187 191
@@ -197,6 +201,7 @@ void ConfigureUi::SetConfiguration() {
197 ui->show_compat->setChecked(UISettings::values.show_compat.GetValue()); 201 ui->show_compat->setChecked(UISettings::values.show_compat.GetValue());
198 ui->show_size->setChecked(UISettings::values.show_size.GetValue()); 202 ui->show_size->setChecked(UISettings::values.show_size.GetValue());
199 ui->show_types->setChecked(UISettings::values.show_types.GetValue()); 203 ui->show_types->setChecked(UISettings::values.show_types.GetValue());
204 ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue());
200 ui->game_icon_size_combobox->setCurrentIndex( 205 ui->game_icon_size_combobox->setCurrentIndex(
201 ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue())); 206 ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue()));
202 ui->folder_icon_size_combobox->setCurrentIndex( 207 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 @@
105 </widget> 105 </widget>
106 </item> 106 </item>
107 <item> 107 <item>
108 <widget class="QCheckBox" name="show_play_time">
109 <property name="text">
110 <string>Show Play Time Column</string>
111 </property>
112 </widget>
113 </item>
114 <item>
108 <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2"> 115 <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2">
109 <item> 116 <item>
110 <widget class="QLabel" name="game_icon_size_label"> 117 <widget class="QLabel" name="game_icon_size_label">
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index b5a02700d..98e410e0f 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -340,6 +340,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
340 340
341 tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); 341 tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
342 tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); 342 tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
343 tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
343 item_model->setSortRole(GameListItemPath::SortRole); 344 item_model->setSortRole(GameListItemPath::SortRole);
344 345
345 connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); 346 connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
@@ -548,6 +549,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
548 QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); 549 QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
549 QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC")); 550 QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC"));
550 QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration")); 551 QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration"));
552 QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data"));
551 QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage")); 553 QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage"));
552 QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache")); 554 QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache"));
553 QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache")); 555 QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache"));
@@ -619,6 +621,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
619 connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { 621 connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
620 emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path); 622 emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path);
621 }); 623 });
624 connect(remove_play_time_data, &QAction::triggered,
625 [this, program_id]() { emit RemovePlayTimeRequested(program_id); });
622 connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { 626 connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
623 emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path); 627 emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path);
624 }); 628 });
@@ -785,6 +789,7 @@ void GameList::RetranslateUI() {
785 item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons")); 789 item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
786 item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); 790 item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
787 item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); 791 item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
792 item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
788} 793}
789 794
790void GameListSearchField::changeEvent(QEvent* event) { 795void GameListSearchField::changeEvent(QEvent* event) {
@@ -812,6 +817,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
812 tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); 817 tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
813 tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); 818 tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
814 tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); 819 tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
820 tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
815 821
816 // Delete any rows that might already exist if we're repopulating 822 // Delete any rows that might already exist if we're repopulating
817 item_model->removeRows(0, item_model->rowCount()); 823 item_model->removeRows(0, item_model->rowCount());
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index 6c2f75e53..cde6f1e1f 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -74,6 +74,7 @@ public:
74 COLUMN_ADD_ONS, 74 COLUMN_ADD_ONS,
75 COLUMN_FILE_TYPE, 75 COLUMN_FILE_TYPE,
76 COLUMN_SIZE, 76 COLUMN_SIZE,
77 COLUMN_PLAY_TIME,
77 COLUMN_COUNT, // Number of columns 78 COLUMN_COUNT, // Number of columns
78 }; 79 };
79 80
@@ -112,6 +113,7 @@ signals:
112 void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type); 113 void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type);
113 void RemoveFileRequested(u64 program_id, GameListRemoveTarget target, 114 void RemoveFileRequested(u64 program_id, GameListRemoveTarget target,
114 const std::string& game_path); 115 const std::string& game_path);
116 void RemovePlayTimeRequested(u64 program_id);
115 void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); 117 void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
116 void CopyTIDRequested(u64 program_id); 118 void CopyTIDRequested(u64 program_id);
117 void CreateShortcut(u64 program_id, const std::string& game_path, 119 void CreateShortcut(u64 program_id, const std::string& game_path,
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index 1800f090f..33a929aae 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -18,6 +18,7 @@
18#include "common/common_types.h" 18#include "common/common_types.h"
19#include "common/logging/log.h" 19#include "common/logging/log.h"
20#include "common/string_util.h" 20#include "common/string_util.h"
21#include "yuzu/play_time.h"
21#include "yuzu/uisettings.h" 22#include "yuzu/uisettings.h"
22#include "yuzu/util/util.h" 23#include "yuzu/util/util.h"
23 24
@@ -221,6 +222,31 @@ public:
221 } 222 }
222}; 223};
223 224
225/**
226 * GameListItem for Play Time values.
227 * This object stores the play time of a game in seconds, and its readable
228 * representation in minutes/hours
229 */
230class GameListItemPlayTime : public GameListItem {
231public:
232 static constexpr int PlayTimeRole = SortRole;
233
234 GameListItemPlayTime() = default;
235 explicit GameListItemPlayTime(const qulonglong time_seconds) {
236 setData(time_seconds, PlayTimeRole);
237 }
238
239 void setData(const QVariant& value, int role) override {
240 qulonglong time_seconds = value.toULongLong();
241 GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole);
242 GameListItem::setData(value, PlayTimeRole);
243 }
244
245 bool operator<(const QStandardItem& other) const override {
246 return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
247 }
248};
249
224class GameListDir : public GameListItem { 250class GameListDir : public GameListItem {
225public: 251public:
226 static constexpr int GameDirRole = Qt::UserRole + 2; 252 static constexpr int GameDirRole = Qt::UserRole + 2;
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index e7fb8a282..b15ed730e 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -27,6 +27,7 @@
27#include "yuzu/game_list.h" 27#include "yuzu/game_list.h"
28#include "yuzu/game_list_p.h" 28#include "yuzu/game_list_p.h"
29#include "yuzu/game_list_worker.h" 29#include "yuzu/game_list_worker.h"
30#include "yuzu/play_time.h"
30#include "yuzu/uisettings.h" 31#include "yuzu/uisettings.h"
31 32
32namespace { 33namespace {
@@ -212,6 +213,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
212 new GameListItemCompat(compatibility), 213 new GameListItemCompat(compatibility),
213 new GameListItem(file_type_string), 214 new GameListItem(file_type_string),
214 new GameListItemSize(size), 215 new GameListItemSize(size),
216 new GameListItemPlayTime(PlayTime::GetPlayTime(program_id)),
215 }; 217 };
216 218
217 const auto patch_versions = GetGameListCachedObject( 219 const auto patch_versions = GetGameListCachedObject(
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 33c9fd0af..53ab7ada9 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -146,6 +146,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
146#include "yuzu/install_dialog.h" 146#include "yuzu/install_dialog.h"
147#include "yuzu/loading_screen.h" 147#include "yuzu/loading_screen.h"
148#include "yuzu/main.h" 148#include "yuzu/main.h"
149#include "yuzu/play_time.h"
149#include "yuzu/startup_checks.h" 150#include "yuzu/startup_checks.h"
150#include "yuzu/uisettings.h" 151#include "yuzu/uisettings.h"
151#include "yuzu/util/clickable_label.h" 152#include "yuzu/util/clickable_label.h"
@@ -334,6 +335,8 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
334 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); 335 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
335 discord_rpc->Update(); 336 discord_rpc->Update();
336 337
338 play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
339
337 system->GetRoomNetwork().Init(); 340 system->GetRoomNetwork().Init();
338 341
339 RegisterMetaTypes(); 342 RegisterMetaTypes();
@@ -1446,6 +1449,8 @@ void GMainWindow::ConnectWidgetEvents() {
1446 connect(game_list, &GameList::RemoveInstalledEntryRequested, this, 1449 connect(game_list, &GameList::RemoveInstalledEntryRequested, this,
1447 &GMainWindow::OnGameListRemoveInstalledEntry); 1450 &GMainWindow::OnGameListRemoveInstalledEntry);
1448 connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); 1451 connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
1452 connect(game_list, &GameList::RemovePlayTimeRequested, this,
1453 &GMainWindow::OnGameListRemovePlayTimeData);
1449 connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); 1454 connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
1450 connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); 1455 connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
1451 connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, 1456 connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
@@ -2458,6 +2463,20 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
2458 } 2463 }
2459} 2464}
2460 2465
2466void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
2467 if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
2468 QMessageBox::Yes | QMessageBox::No,
2469 QMessageBox::No) != QMessageBox::Yes) {
2470 return;
2471 }
2472 if (!play_time_manager->ResetProgramPlayTime(program_id)) {
2473 QMessageBox::warning(this, tr("Error Resetting Play Time Data"),
2474 tr("Play time couldn't be cleared"));
2475 return;
2476 }
2477 game_list->PopulateAsync(UISettings::values.game_dirs);
2478}
2479
2461void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) { 2480void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) {
2462 const auto target_file_name = [target] { 2481 const auto target_file_name = [target] {
2463 switch (target) { 2482 switch (target) {
@@ -3230,6 +3249,9 @@ void GMainWindow::OnStartGame() {
3230 UpdateMenuState(); 3249 UpdateMenuState();
3231 OnTasStateChanged(); 3250 OnTasStateChanged();
3232 3251
3252 play_time_manager->SetProgramId(system->GetApplicationProcessProgramID());
3253 play_time_manager->Start();
3254
3233 discord_rpc->Update(); 3255 discord_rpc->Update();
3234} 3256}
3235 3257
@@ -3245,6 +3267,7 @@ void GMainWindow::OnRestartGame() {
3245 3267
3246void GMainWindow::OnPauseGame() { 3268void GMainWindow::OnPauseGame() {
3247 emu_thread->SetRunning(false); 3269 emu_thread->SetRunning(false);
3270 play_time_manager->Stop();
3248 UpdateMenuState(); 3271 UpdateMenuState();
3249 AllowOSSleep(); 3272 AllowOSSleep();
3250} 3273}
@@ -3265,6 +3288,9 @@ void GMainWindow::OnStopGame() {
3265 return; 3288 return;
3266 } 3289 }
3267 3290
3291 play_time_manager->Stop();
3292 // Update game list to show new play time
3293 game_list->PopulateAsync(UISettings::values.game_dirs);
3268 if (OnShutdownBegin()) { 3294 if (OnShutdownBegin()) {
3269 OnShutdownBeginDialog(); 3295 OnShutdownBeginDialog();
3270 } else { 3296 } else {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 1b7055122..c3003f8d9 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -76,6 +76,10 @@ namespace DiscordRPC {
76class DiscordInterface; 76class DiscordInterface;
77} 77}
78 78
79namespace PlayTime {
80class PlayTimeManager;
81}
82
79namespace FileSys { 83namespace FileSys {
80class ContentProvider; 84class ContentProvider;
81class ManualContentProvider; 85class ManualContentProvider;
@@ -312,6 +316,7 @@ private slots:
312 void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); 316 void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type);
313 void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, 317 void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target,
314 const std::string& game_path); 318 const std::string& game_path);
319 void OnGameListRemovePlayTimeData(u64 program_id);
315 void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); 320 void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
316 void OnGameListCopyTID(u64 program_id); 321 void OnGameListCopyTID(u64 program_id);
317 void OnGameListNavigateToGamedbEntry(u64 program_id, 322 void OnGameListNavigateToGamedbEntry(u64 program_id,
@@ -374,6 +379,7 @@ private:
374 void RemoveVulkanDriverPipelineCache(u64 program_id); 379 void RemoveVulkanDriverPipelineCache(u64 program_id);
375 void RemoveAllTransferableShaderCaches(u64 program_id); 380 void RemoveAllTransferableShaderCaches(u64 program_id);
376 void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); 381 void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
382 void RemovePlayTimeData(u64 program_id);
377 void RemoveCacheStorage(u64 program_id); 383 void RemoveCacheStorage(u64 program_id);
378 std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id); 384 std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id);
379 InstallResult InstallNSPXCI(const QString& filename); 385 InstallResult InstallNSPXCI(const QString& filename);
@@ -411,6 +417,7 @@ private:
411 417
412 std::unique_ptr<Core::System> system; 418 std::unique_ptr<Core::System> system;
413 std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; 419 std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
420 std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
414 std::shared_ptr<InputCommon::InputSubsystem> input_subsystem; 421 std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
415 422
416 MultiplayerState* multiplayer_state = nullptr; 423 MultiplayerState* multiplayer_state = nullptr;
diff --git a/src/yuzu/play_time.cpp b/src/yuzu/play_time.cpp
new file mode 100644
index 000000000..6be0327b2
--- /dev/null
+++ b/src/yuzu/play_time.cpp
@@ -0,0 +1,177 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "common/fs/file.h"
5#include "common/fs/path_util.h"
6#include "common/logging/log.h"
7#include "common/settings.h"
8#include "common/thread.h"
9#include "core/hle/service/acc/profile_manager.h"
10
11#include "yuzu/play_time.h"
12
13namespace PlayTime {
14
15void PlayTimeManager::SetProgramId(u64 program_id) {
16 this->running_program_id = program_id;
17}
18
19inline void PlayTimeManager::UpdateTimestamp() {
20 this->last_timestamp = std::chrono::steady_clock::now();
21}
22
23void PlayTimeManager::Start() {
24 UpdateTimestamp();
25 play_time_thread =
26 std::jthread([&](std::stop_token stop_token) { this->AutoTimestamp(stop_token); });
27}
28
29void PlayTimeManager::Stop() {
30 play_time_thread.request_stop();
31}
32
33void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
34 Common::SetCurrentThreadName("PlayTimeReport");
35
36 using namespace std::literals::chrono_literals;
37
38 const auto duration = 30s;
39 while (Common::StoppableTimedWait(stop_token, duration)) {
40 Save();
41 }
42
43 Save();
44}
45
46void PlayTimeManager::Save() {
47 const auto now = std::chrono::steady_clock::now();
48 const auto duration =
49 static_cast<u64>(std::chrono::duration_cast<std::chrono::seconds>(
50 std::chrono::steady_clock::duration(now - this->last_timestamp))
51 .count());
52 UpdateTimestamp();
53 if (!UpdatePlayTime(running_program_id, duration)) {
54 LOG_ERROR(Common, "Failed to update play time");
55 }
56}
57
58bool UpdatePlayTime(u64 program_id, u64 add_play_time) {
59 std::vector<PlayTimeElement> play_time_elements;
60 if (!ReadPlayTimeFile(play_time_elements)) {
61 return false;
62 }
63 const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id);
64
65 if (it == play_time_elements.end()) {
66 play_time_elements.push_back({.program_id = program_id, .play_time = add_play_time});
67 } else {
68 play_time_elements.at(it - play_time_elements.begin()).play_time += add_play_time;
69 }
70 if (!WritePlayTimeFile(play_time_elements)) {
71 return false;
72 }
73 return true;
74}
75
76u64 GetPlayTime(u64 program_id) {
77 std::vector<PlayTimeElement> play_time_elements;
78
79 if (!ReadPlayTimeFile(play_time_elements)) {
80 return 0;
81 }
82 const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id);
83 if (it == play_time_elements.end()) {
84 return 0;
85 }
86 return play_time_elements.at(it - play_time_elements.begin()).play_time;
87}
88
89bool PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
90 std::vector<PlayTimeElement> play_time_elements;
91
92 if (!ReadPlayTimeFile(play_time_elements)) {
93 return false;
94 }
95 const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id);
96 if (it == play_time_elements.end()) {
97 return false;
98 }
99 play_time_elements.erase(it);
100 if (!WritePlayTimeFile(play_time_elements)) {
101 return false;
102 }
103 return true;
104}
105
106std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() {
107 const Service::Account::ProfileManager manager;
108 const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
109 if (!uuid.has_value()) {
110 return std::nullopt;
111 }
112 return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) /
113 uuid->RawString().append(".bin");
114}
115
116[[nodiscard]] bool ReadPlayTimeFile(std::vector<PlayTimeElement>& out_play_time_elements) {
117 const auto filename = GetCurrentUserPlayTimePath();
118 if (!filename.has_value()) {
119 LOG_ERROR(Common, "Failed to get current user path");
120 return false;
121 }
122
123 if (Common::FS::Exists(filename.value())) {
124 Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read,
125 Common::FS::FileType::BinaryFile};
126 if (!file.IsOpen()) {
127 LOG_ERROR(Common, "Failed to open play time file: {}",
128 Common::FS::PathToUTF8String(filename.value()));
129 return false;
130 }
131 const size_t elem_num = file.GetSize() / sizeof(PlayTimeElement);
132 out_play_time_elements.resize(elem_num);
133 const bool success = file.ReadSpan<PlayTimeElement>(out_play_time_elements) == elem_num;
134 file.Close();
135 return success;
136 } else {
137 out_play_time_elements.clear();
138 return true;
139 }
140}
141
142[[nodiscard]] bool WritePlayTimeFile(const std::vector<PlayTimeElement>& play_time_elements) {
143 const auto filename = GetCurrentUserPlayTimePath();
144 if (!filename.has_value()) {
145 LOG_ERROR(Common, "Failed to get current user path");
146 return false;
147 }
148 Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write,
149 Common::FS::FileType::BinaryFile};
150
151 if (!file.IsOpen()) {
152 LOG_ERROR(Common, "Failed to open play time file: {}",
153 Common::FS::PathToUTF8String(filename.value()));
154 return false;
155 }
156 const bool success =
157 file.WriteSpan<PlayTimeElement>(play_time_elements) == play_time_elements.size();
158 file.Close();
159 return success;
160}
161
162QString ReadablePlayTime(qulonglong time_seconds) {
163 static constexpr std::array units{"m", "h"};
164 if (time_seconds == 0) {
165 return QLatin1String("");
166 }
167 const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0);
168 const auto time_hours = static_cast<double>(time_seconds) / 3600;
169 const int unit = time_minutes < 60 ? 0 : 1;
170 const auto value = unit == 0 ? time_minutes : time_hours;
171
172 return QStringLiteral("%L1 %2")
173 .arg(value, 0, 'f', unit && time_seconds % 60 != 0)
174 .arg(QString::fromUtf8(units[unit]));
175}
176
177} // namespace PlayTime
diff --git a/src/yuzu/play_time.h b/src/yuzu/play_time.h
new file mode 100644
index 000000000..68e40955c
--- /dev/null
+++ b/src/yuzu/play_time.h
@@ -0,0 +1,68 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <QString>
7
8#include <atomic>
9#include <condition_variable>
10#include <mutex>
11#include <optional>
12#include <thread>
13
14#include "common/common_types.h"
15#include "common/fs/fs.h"
16#include "common/polyfill_thread.h"
17#include "core/core.h"
18
19namespace PlayTime {
20struct PlayTimeElement {
21 u64 program_id;
22 u64 play_time;
23
24 inline bool operator==(const PlayTimeElement& other) const {
25 return program_id == other.program_id;
26 }
27
28 inline bool operator==(const u64 _program_id) const {
29 return program_id == _program_id;
30 }
31};
32
33class PlayTimeManager {
34public:
35 explicit PlayTimeManager() = default;
36 ~PlayTimeManager() = default;
37
38public:
39 YUZU_NON_COPYABLE(PlayTimeManager);
40 YUZU_NON_MOVEABLE(PlayTimeManager);
41
42public:
43 bool ResetProgramPlayTime(u64 program_id);
44 void SetProgramId(u64 program_id);
45 inline void UpdateTimestamp();
46 void Start();
47 void Stop();
48
49private:
50 u64 running_program_id;
51 std::chrono::steady_clock::time_point last_timestamp;
52 std::jthread play_time_thread;
53 void AutoTimestamp(std::stop_token stop_token);
54 void Save();
55};
56
57std::optional<std::filesystem::path> GetCurrentUserPlayTimePath();
58
59bool UpdatePlayTime(u64 program_id, u64 add_play_time);
60
61[[nodiscard]] bool ReadPlayTimeFile(std::vector<PlayTimeElement>& out_play_time_elements);
62[[nodiscard]] bool WritePlayTimeFile(const std::vector<PlayTimeElement>& play_time_elements);
63
64u64 GetPlayTime(u64 program_id);
65
66QString ReadablePlayTime(qulonglong time_seconds);
67
68} // namespace PlayTime
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index 8efd63f31..848631a9e 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -183,6 +183,9 @@ struct Values {
183 Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList}; 183 Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList};
184 Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList}; 184 Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList};
185 185
186 // Play time
187 Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList};
188
186 bool configuration_applied; 189 bool configuration_applied;
187 bool reset_to_defaults; 190 bool reset_to_defaults;
188 bool shortcut_already_warned{false}; 191 bool shortcut_already_warned{false};