From b4662a822bf713ac93773a22c0d6161c8f1970d1 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Thu, 8 Feb 2024 00:17:16 +0100 Subject: [PATCH] lime_qt: Track play time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mario Davó <66087392+mdmrk@users.noreply.github.com> --- src/common/common_paths.h | 1 + src/common/file_util.cpp | 1 + src/common/file_util.h | 54 +++++++++ src/common/polyfill_thread.h | 39 +++++++ src/lime_qt/CMakeLists.txt | 2 + src/lime_qt/configuration/config.cpp | 2 + src/lime_qt/game_list.cpp | 13 ++- src/lime_qt/game_list.h | 7 +- src/lime_qt/game_list_p.h | 26 +++++ src/lime_qt/game_list_worker.cpp | 7 +- src/lime_qt/game_list_worker.h | 5 +- src/lime_qt/main.cpp | 32 +++++- src/lime_qt/main.h | 9 ++ src/lime_qt/play_time_manager.cpp | 158 +++++++++++++++++++++++++++ src/lime_qt/play_time_manager.h | 45 ++++++++ src/lime_qt/uisettings.h | 1 + 16 files changed, 394 insertions(+), 8 deletions(-) create mode 100644 src/lime_qt/play_time_manager.cpp create mode 100644 src/lime_qt/play_time_manager.h diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 71c4de06f..4c96158c9 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -53,6 +53,7 @@ #define SHADER_DIR "shaders" #define STATES_DIR "states" #define ICONS_DIR "icons" +#define PLAY_TIME_DIR "play_time" // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index fd3931c10..7f7ce34cf 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -827,6 +827,7 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP); g_paths.emplace(UserPath::IconsDir, user_path + ICONS_DIR DIR_SEP); + g_paths.emplace(UserPath::PlayTimeDir, user_path + PLAY_TIME_DIR DIR_SEP); g_default_paths = g_paths; } diff --git a/src/common/file_util.h b/src/common/file_util.h index 5e1ccae6a..213319ca6 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -41,6 +41,7 @@ enum class UserPath { SysDataDir, UserDir, IconsDir, + PlayTimeDir, }; // Replaces install-specific paths with standard placeholders, and back again @@ -344,6 +345,59 @@ public: return WriteArray(str.data(), str.length()); } + /** + * Reads a span of T data from a file sequentially. + * This function reads from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully read. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks read permissions + * - Attempting to read beyond the end-of-file + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully read. + */ + template + [[nodiscard]] size_t ReadSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fread(data.data(), sizeof(T), data.size(), m_file); + } + + /** + * Writes a span of T data to a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully written. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks write permissions + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully written. + */ + template + [[nodiscard]] size_t WriteSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fwrite(data.data(), sizeof(T), data.size(), m_file); + } + [[nodiscard]] bool IsOpen() const { return nullptr != m_file; } diff --git a/src/common/polyfill_thread.h b/src/common/polyfill_thread.h index 3146075f3..bf8cb4ecb 100644 --- a/src/common/polyfill_thread.h +++ b/src/common/polyfill_thread.h @@ -12,8 +12,11 @@ #ifdef __cpp_lib_jthread +#include +#include #include #include +#include namespace Common { @@ -22,11 +25,23 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) { cv.wait(lock, token, std::move(pred)); } +template +bool StoppableTimedWait(std::stop_token token, const std::chrono::duration& rel_time) { + std::condition_variable_any cv; + std::mutex m; + + // Perform the timed wait. + std::unique_lock lk{m}; + return !cv.wait_for(lk, token, rel_time, [&] { return token.stop_requested(); }); +} + } // namespace Common #else #include +#include +#include #include #include #include @@ -333,6 +348,30 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) { cv.wait(lock, [&] { return pred() || token.stop_requested(); }); } +template +bool StoppableTimedWait(std::stop_token token, const std::chrono::duration& rel_time) { + if (token.stop_requested()) { + return false; + } + + bool stop_requested = false; + std::condition_variable cv; + std::mutex m; + + std::stop_callback cb(token, [&] { + // Wake up the waiting thread. + { + std::scoped_lock lk{m}; + stop_requested = true; + } + cv.notify_one(); + }); + + // Perform the timed wait. + std::unique_lock lk{m}; + return !cv.wait_for(lk, rel_time, [&] { return stop_requested; }); +} + } // namespace Common #endif // __cpp_lib_jthread diff --git a/src/lime_qt/CMakeLists.txt b/src/lime_qt/CMakeLists.txt index e791e32bd..13ea48982 100644 --- a/src/lime_qt/CMakeLists.txt +++ b/src/lime_qt/CMakeLists.txt @@ -172,6 +172,8 @@ add_executable(lime-qt multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h + play_time_manager.cpp + play_time_manager.h precompiled_headers.h uisettings.cpp uisettings.h diff --git a/src/lime_qt/configuration/config.cpp b/src/lime_qt/configuration/config.cpp index 728a00c8c..96d49c742 100644 --- a/src/lime_qt/configuration/config.cpp +++ b/src/lime_qt/configuration/config.cpp @@ -804,6 +804,7 @@ void Config::ReadUIGameListValues() { ReadBasicSetting(UISettings::values.show_region_column); ReadBasicSetting(UISettings::values.show_type_column); ReadBasicSetting(UISettings::values.show_size_column); + ReadBasicSetting(UISettings::values.show_play_time_column); const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites")); for (int i = 0; i < favorites_size; i++) { @@ -1293,6 +1294,7 @@ void Config::SaveUIGameListValues() { WriteBasicSetting(UISettings::values.show_region_column); WriteBasicSetting(UISettings::values.show_type_column); WriteBasicSetting(UISettings::values.show_size_column); + WriteBasicSetting(UISettings::values.show_play_time_column); qt_config->beginWriteArray(QStringLiteral("favorites")); for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) { diff --git a/src/lime_qt/game_list.cpp b/src/lime_qt/game_list.cpp index 181dfaee6..8e5533aed 100644 --- a/src/lime_qt/game_list.cpp +++ b/src/lime_qt/game_list.cpp @@ -306,7 +306,8 @@ void GameList::OnFilterCloseClicked() { main_window->filterBarSetChecked(false); } -GameList::GameList(GMainWindow* parent) : QWidget{parent} { +GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent) + : QWidget{parent}, play_time_manager{play_time_manager_} { watcher = new QFileSystemWatcher(this); connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory, Qt::UniqueConnection); @@ -522,7 +523,8 @@ void GameList::PopupHeaderContextMenu(const QPoint& menu_location) { {tr("Compatibility"), &UISettings::values.show_compat_column}, {tr("Region"), &UISettings::values.show_region_column}, {tr("File type"), &UISettings::values.show_type_column}, - {tr("Size"), &UISettings::values.show_size_column}}; + {tr("Size"), &UISettings::values.show_size_column}, + {tr("Play time"), &UISettings::values.show_play_time_column}}; QActionGroup* column_group = new QActionGroup(this); column_group->setExclusive(false); @@ -544,6 +546,7 @@ void GameList::UpdateColumnVisibility() { tree_view->setColumnHidden(COLUMN_REGION, !UISettings::values.show_region_column); tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_type_column); tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size_column); + tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time_column); } #ifdef ENABLE_OPENGL @@ -591,6 +594,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QAction* uninstall_update = uninstall_menu->addAction(tr("Update")); QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); + QAction* remove_play_time_data = context_menu.addAction(tr("Remove Play Time Data")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); #if !defined(__APPLE__) @@ -712,6 +716,8 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr }); connect(dump_romfs, &QAction::triggered, this, [this, path, program_id] { emit DumpRomFSRequested(path, program_id); }); + connect(remove_play_time_data, &QAction::triggered, + [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); @@ -933,6 +939,7 @@ void GameList::RetranslateUI() { item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, tr("Region")); 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) { @@ -964,7 +971,7 @@ void GameList::PopulateAsync(QVector& game_dirs) { emit ShouldCancelWorker(); - GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list); + GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list, play_time_manager); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, diff --git a/src/lime_qt/game_list.h b/src/lime_qt/game_list.h index 6fa4e6fb7..4bc03d96b 100644 --- a/src/lime_qt/game_list.h +++ b/src/lime_qt/game_list.h @@ -10,6 +10,7 @@ #include #include "common/common_types.h" #include "lime_qt/compatibility_list.h" +#include "lime_qt/play_time_manager.h" #include "uisettings.h" namespace Service::FS { @@ -60,10 +61,11 @@ public: COLUMN_REGION, COLUMN_FILE_TYPE, COLUMN_SIZE, + COLUMN_PLAY_TIME, COLUMN_COUNT, // Number of columns }; - explicit GameList(GMainWindow* parent = nullptr); + explicit GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent = nullptr); ~GameList() override; QString GetLastFilterResultItem() const; @@ -97,6 +99,7 @@ signals: void OpenFolderRequested(u64 program_id, GameListOpenTarget target); void CreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target); + void RemovePlayTimeRequested(u64 program_id); void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const QString file); @@ -142,6 +145,8 @@ private: CompatibilityList compatibility_list; friend class GameListSearchField; + + const PlayTime::PlayTimeManager& play_time_manager; }; Q_DECLARE_METATYPE(GameListOpenTarget); diff --git a/src/lime_qt/game_list_p.h b/src/lime_qt/game_list_p.h index 48924819e..d165c007c 100644 --- a/src/lime_qt/game_list_p.h +++ b/src/lime_qt/game_list_p.h @@ -22,6 +22,7 @@ #include "common/logging/log.h" #include "common/string_util.h" #include "core/loader/smdh.h" +#include "lime_qt/play_time_manager.h" #include "lime_qt/uisettings.h" #include "lime_qt/util/util.h" @@ -362,6 +363,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/lime_qt/game_list_worker.cpp b/src/lime_qt/game_list_worker.cpp index 0a7e66808..e95584e82 100644 --- a/src/lime_qt/game_list_worker.cpp +++ b/src/lime_qt/game_list_worker.cpp @@ -27,8 +27,10 @@ bool HasSupportedFileExtension(const std::string& file_name) { } // Anonymous namespace GameListWorker::GameListWorker(QVector& game_dirs, - const CompatibilityList& compatibility_list) - : game_dirs(game_dirs), compatibility_list(compatibility_list) {} + const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager_) + : game_dirs(game_dirs), compatibility_list(compatibility_list), + play_time_manager{play_time_manager_} {} GameListWorker::~GameListWorker() = default; @@ -112,6 +114,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(FileUtil::GetSize(physical_name)), + new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), }, parent_dir); diff --git a/src/lime_qt/game_list_worker.h b/src/lime_qt/game_list_worker.h index 68d8f49a1..3d6e1364e 100644 --- a/src/lime_qt/game_list_worker.h +++ b/src/lime_qt/game_list_worker.h @@ -14,6 +14,7 @@ #include #include "common/common_types.h" #include "lime_qt/compatibility_list.h" +#include "lime_qt/play_time_manager.h" namespace Service::FS { enum class MediaType : u32; @@ -30,7 +31,8 @@ class GameListWorker : public QObject, public QRunnable { public: GameListWorker(QVector& game_dirs, - const CompatibilityList& compatibility_list); + const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager_); ~GameListWorker() override; /// Starts the processing of directory tree information. @@ -60,6 +62,7 @@ private: QVector& game_dirs; const CompatibilityList& compatibility_list; + const PlayTime::PlayTimeManager& play_time_manager; QStringList watch_list; std::atomic_bool stop_processing; diff --git a/src/lime_qt/main.cpp b/src/lime_qt/main.cpp index 81102ace9..a9bb2a4c5 100644 --- a/src/lime_qt/main.cpp +++ b/src/lime_qt/main.cpp @@ -71,6 +71,7 @@ #include "lime_qt/movie/movie_play_dialog.h" #include "lime_qt/movie/movie_record_dialog.h" #include "lime_qt/multiplayer/state.h" +#include "lime_qt/play_time_manager.h" #include "lime_qt/qt_image_interface.h" #include "lime_qt/uisettings.h" #include "lime_qt/updater/updater.h" @@ -188,6 +189,8 @@ GMainWindow::GMainWindow(Core::System& system_) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + play_time_manager = std::make_unique(); + Network::Init(); movie.SetPlaybackCompletionCallback([this] { @@ -339,7 +342,7 @@ void GMainWindow::InitializeWidgets() { secondary_window->hide(); secondary_window->setParent(nullptr); - game_list = new GameList(this); + game_list = new GameList(*play_time_manager, this); ui->horizontalLayout->addWidget(game_list); game_list_placeholder = new GameListPlaceholder(this); @@ -825,6 +828,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::RemovePlayTimeRequested, this, + &GMainWindow::OnGameListRemovePlayTimeData); connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, &GMainWindow::OnGameListNavigateToGamedbEntry); connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); @@ -1223,7 +1228,11 @@ bool GMainWindow::LoadROM(const QString& filename) { game_title_long = QString::fromStdString(title_long); UpdateWindowTitle(); + u64 title_id; + system.GetAppLoader().ReadProgramId(title_id); + game_path = filename; + game_title_id = title_id; return true; } @@ -1445,6 +1454,7 @@ void GMainWindow::ShutdownGame() { UpdateWindowTitle(); game_path.clear(); + game_title_id = 0; // Update the GUI UpdateMenuState(); @@ -1632,6 +1642,17 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); } +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::OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list) { auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); @@ -2165,6 +2186,9 @@ void GMainWindow::OnStartGame() { UpdateMenuState(); + play_time_manager->SetProgramId(game_title_id); + play_time_manager->Start(); + discord_rpc->Update(); #ifdef __unix__ @@ -2187,6 +2211,8 @@ void GMainWindow::OnPauseGame() { emu_thread->SetRunning(false); qt_cameras->PauseCameras(); + play_time_manager->Stop(); + UpdateMenuState(); AllowOSSleep(); @@ -2206,6 +2232,10 @@ void GMainWindow::OnPauseContinueGame() { } void GMainWindow::OnStopGame() { + play_time_manager->Stop(); + // Update game list to show new play time + game_list->PopulateAsync(UISettings::values.game_dirs); + ShutdownGame(); graphics_api_button->setEnabled(true); Settings::RestoreGlobalState(false); diff --git a/src/lime_qt/main.h b/src/lime_qt/main.h index e638a9b34..a25d2ac29 100644 --- a/src/lime_qt/main.h +++ b/src/lime_qt/main.h @@ -64,6 +64,10 @@ namespace DiscordRPC { class DiscordInterface; } +namespace PlayTime { +class PlayTimeManager; +} + namespace Core { class Movie; } @@ -94,6 +98,7 @@ public: ~GMainWindow(); GameList* game_list; + std::unique_ptr play_time_manager; std::unique_ptr discord_rpc; bool DropAction(QDropEvent* event); @@ -224,6 +229,7 @@ private slots: /// Called whenever a user selects a game in the game list widget. void OnGameListLoadFile(QString game_path); void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); + void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, @@ -298,6 +304,7 @@ private: void UpdateWindowTitle(); void UpdateUISettings(); void RetranslateStatusBar(); + void RemovePlayTimeData(u64 program_id); void InstallCIA(QStringList filepaths); void HideMouseCursor(); void ShowMouseCursor(); @@ -343,6 +350,8 @@ private: QString game_title_long; // The path to the game currently running QString game_path; + // The title id of the game currently running + u64 game_title_id; bool auto_paused = false; bool auto_muted = false; diff --git a/src/lime_qt/play_time_manager.cpp b/src/lime_qt/play_time_manager.cpp new file mode 100644 index 000000000..70112bbc3 --- /dev/null +++ b/src/lime_qt/play_time_manager.cpp @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2024 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/alignment.h" +#include "common/common_paths.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/thread.h" +#include "lime_qt/play_time_manager.h" + +namespace PlayTime { + +namespace { + +struct PlayTimeElement { + ProgramId program_id; + PlayTime play_time; +}; + +std::string GetCurrentUserPlayTimePath() { + return FileUtil::GetUserPath(FileUtil::UserPath::PlayTimeDir) + DIR_SEP + "play_time.bin"; +} + +[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); + + out_play_time_db.clear(); + + if (FileUtil::Exists(filename)) { + FileUtil::IOFile file{filename, "rb"}; + if (!file.IsOpen()) { + LOG_ERROR(Frontend, "Failed to open play time file: {}", filename); + return false; + } + + const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement); + std::vector elements(num_elements); + + if (file.ReadSpan(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(); + + FileUtil::IOFile file{filename, "wb"}; + if (!file.IsOpen()) { + LOG_ERROR(Frontend, "Failed to open play time file: {}", filename); + return false; + } + + std::vector 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(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(timestamp - last_timestamp); + return static_cast(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(time_seconds) / 60, 1.0); + const auto time_hours = static_cast(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/lime_qt/play_time_manager.h b/src/lime_qt/play_time_manager.h new file mode 100644 index 000000000..c8ba48db7 --- /dev/null +++ b/src/lime_qt/play_time_manager.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#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; + +class PlayTimeManager { +public: + explicit PlayTimeManager(); + ~PlayTimeManager(); + + PlayTimeManager(const PlayTimeManager&) = delete; + PlayTimeManager& operator=(const PlayTimeManager&) = delete; + + u64 GetPlayTime(u64 program_id) const; + void ResetProgramPlayTime(u64 program_id); + void SetProgramId(u64 program_id); + void Start(); + void Stop(); + +private: + void AutoTimestamp(std::stop_token stop_token); + void Save(); + + PlayTimeDatabase database; + u64 running_program_id; + std::jthread play_time_thread; +}; + +QString ReadablePlayTime(qulonglong time_seconds); + +} // namespace PlayTime diff --git a/src/lime_qt/uisettings.h b/src/lime_qt/uisettings.h index f83435070..e75f57545 100644 --- a/src/lime_qt/uisettings.h +++ b/src/lime_qt/uisettings.h @@ -103,6 +103,7 @@ struct Values { Settings::Setting show_region_column{true, "show_region_column"}; Settings::Setting show_type_column{true, "show_type_column"}; Settings::Setting show_size_column{true, "show_size_column"}; + Settings::Setting show_play_time_column{true, "show_play_time_column"}; Settings::Setting screenshot_resolution_factor{0, "screenshot_resolution_factor"}; Settings::SwitchableSetting screenshot_path{"", "screenshotPath"};