From e234f99f42ea45b087796371073bac06b0290b96 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sat, 15 Mar 2025 12:23:17 -0700 Subject: [PATCH 1/7] Rename TimePlayed to TimePlayedManager and make singleton Also add some locks and rename some variables appropriately. --- Source/Android/jni/GameList/GameFile.cpp | 3 ++- Source/Core/Core/HW/CPU.cpp | 4 ++-- Source/Core/Core/TimePlayed.cpp | 23 +++++++++++++++---- Source/Core/Core/TimePlayed.h | 19 ++++++++------- .../Core/DolphinQt/GameList/GameListModel.cpp | 9 ++++---- .../Core/DolphinQt/GameList/GameListModel.h | 2 +- 6 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp index dc922d701c..d8a8e625c2 100644 --- a/Source/Android/jni/GameList/GameFile.cpp +++ b/Source/Android/jni/GameList/GameFile.cpp @@ -195,7 +195,8 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerHe JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTimePlayedMsInternal(JNIEnv* env, jobject obj) { - const std::chrono::milliseconds time = TimePlayed().GetTimePlayed(GetRef(env, obj)->GetGameID()); + const std::chrono::milliseconds time = + TimePlayedManager::GetInstance().GetTimePlayed(GetRef(env, obj)->GetGameID()); return time.count(); } diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 568e3b3894..832162c138 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -78,7 +78,7 @@ void CPUManager::StartTimePlayedTimer() while (true) { - TimePlayed time_played; + auto& time_played_manager = TimePlayedManager::GetInstance(); auto curr_time = timer.now(); // Check that emulation is not paused @@ -88,7 +88,7 @@ void CPUManager::StartTimePlayedTimer() const std::string game_id = SConfig::GetInstance().GetGameID(); const auto diff_time = std::chrono::duration_cast(curr_time - prev_time); - time_played.AddTime(game_id, diff_time); + time_played_manager.AddTime(game_id, diff_time); } else if (m_state == State::Stepping) { diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp index 9323a49393..00e72f4e70 100644 --- a/Source/Core/Core/TimePlayed.cpp +++ b/Source/Core/Core/TimePlayed.cpp @@ -4,6 +4,7 @@ #include "Core/TimePlayed.h" #include +#include #include #include "Common/CommonTypes.h" @@ -11,32 +12,44 @@ #include "Common/IniFile.h" #include "Common/NandPaths.h" -TimePlayed::TimePlayed() : m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") +TimePlayedManager::TimePlayedManager() + : m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") { Reload(); } -TimePlayed::~TimePlayed() = default; +TimePlayedManager::~TimePlayedManager() = default; -void TimePlayed::AddTime(const std::string& game_id, std::chrono::milliseconds time_emulated) +TimePlayedManager& TimePlayedManager::GetInstance() +{ + static TimePlayedManager time_played_manager; + return time_played_manager; +} + +void TimePlayedManager::AddTime(const std::string& game_id, std::chrono::milliseconds time_emulated) { std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; + + std::lock_guard guard(m_mutex); m_time_list->Get(filtered_game_id, &previous_time); m_time_list->Set(filtered_game_id, previous_time + static_cast(time_emulated.count())); m_ini.Save(m_ini_path); } -std::chrono::milliseconds TimePlayed::GetTimePlayed(const std::string& game_id) const +std::chrono::milliseconds TimePlayedManager::GetTimePlayed(const std::string& game_id) const { std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; + + std::lock_guard guard(m_mutex); m_time_list->Get(filtered_game_id, &previous_time); return std::chrono::milliseconds(previous_time); } -void TimePlayed::Reload() +void TimePlayedManager::Reload() { + std::lock_guard guard(m_mutex); m_ini.Load(m_ini_path); m_time_list = m_ini.GetOrCreateSection("TimePlayed"); } diff --git a/Source/Core/Core/TimePlayed.h b/Source/Core/Core/TimePlayed.h index 5d1f279db3..6adfc56348 100644 --- a/Source/Core/Core/TimePlayed.h +++ b/Source/Core/Core/TimePlayed.h @@ -4,23 +4,23 @@ #pragma once #include +#include #include #include "Common/CommonTypes.h" #include "Common/IniFile.h" -class TimePlayed +class TimePlayedManager { public: - TimePlayed(); + TimePlayedManager(const TimePlayedManager& other) = delete; + TimePlayedManager(TimePlayedManager&& other) = delete; + TimePlayedManager& operator=(const TimePlayedManager& other) = delete; + TimePlayedManager& operator=(TimePlayedManager&& other) = delete; - // not copyable due to the stored section pointer - TimePlayed(const TimePlayed& other) = delete; - TimePlayed(TimePlayed&& other) = delete; - TimePlayed& operator=(const TimePlayed& other) = delete; - TimePlayed& operator=(TimePlayed&& other) = delete; + ~TimePlayedManager(); - ~TimePlayed(); + static TimePlayedManager& GetInstance(); void AddTime(const std::string& game_id, std::chrono::milliseconds time_emulated); @@ -29,7 +29,10 @@ public: void Reload(); private: + TimePlayedManager(); + std::string m_ini_path; + mutable std::mutex m_mutex; Common::IniFile m_ini; Common::IniFile::Section* m_time_list; }; diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index 42a911ce49..9ff39f90ed 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -23,7 +23,8 @@ const QSize GAMECUBE_BANNER_SIZE(96, 32); -GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent) +GameListModel::GameListModel(QObject* parent) + : QAbstractTableModel(parent), m_time_played_manager(TimePlayedManager::GetInstance()) { connect(&m_tracker, &GameTracker::GameLoaded, this, &GameListModel::AddGame); connect(&m_tracker, &GameTracker::GameUpdated, this, &GameListModel::UpdateGame); @@ -195,7 +196,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const if (role == Qt::DisplayRole) { const std::string game_id = game.GetGameID(); - const std::chrono::milliseconds total_time = m_timer.GetTimePlayed(game_id); + const std::chrono::milliseconds total_time = m_time_played_manager.GetTimePlayed(game_id); const auto total_minutes = std::chrono::duration_cast(total_time); const auto total_hours = std::chrono::duration_cast(total_time); @@ -207,7 +208,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const if (role == SORT_ROLE) { const std::string game_id = game.GetGameID(); - return static_cast(m_timer.GetTimePlayed(game_id).count()); + return static_cast(m_time_played_manager.GetTimePlayed(game_id).count()); } break; case Column::Tags: @@ -512,6 +513,6 @@ void GameListModel::OnEmulationStateChanged(Core::State state) { if (state == Core::State::Uninitialized) { - m_timer.Reload(); + m_time_played_manager.Reload(); } } diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index baac5d507d..f513a1dde1 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -98,7 +98,7 @@ private: GameTracker m_tracker; QList> m_games; Core::TitleDatabase m_title_database; - TimePlayed m_timer; + TimePlayedManager& m_time_played_manager; QString m_term; float m_scale = 1.0; }; From 0161065959b9db38cb1e57cebf988bd15cc3c6e3 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sun, 16 Mar 2025 14:41:18 -0700 Subject: [PATCH 2/7] GameList: Update time played while game is running Add an UpdateEvent for time played updates and use it to update the Game List time played while a game is running (instead of just when it stops). --- Source/Core/Core/TimePlayed.cpp | 26 +++++++------ Source/Core/Core/TimePlayed.h | 4 +- .../Core/DolphinQt/GameList/GameListModel.cpp | 37 +++++++++++++------ .../Core/DolphinQt/GameList/GameListModel.h | 4 +- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp index 00e72f4e70..f59ad5c702 100644 --- a/Source/Core/Core/TimePlayed.cpp +++ b/Source/Core/Core/TimePlayed.cpp @@ -9,13 +9,15 @@ #include "Common/CommonTypes.h" #include "Common/FileUtil.h" +#include "Common/HookableEvent.h" #include "Common/IniFile.h" #include "Common/NandPaths.h" TimePlayedManager::TimePlayedManager() : m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") { - Reload(); + m_ini.Load(m_ini_path); + m_time_list = m_ini.GetOrCreateSection("TimePlayed"); } TimePlayedManager::~TimePlayedManager() = default; @@ -30,11 +32,18 @@ void TimePlayedManager::AddTime(const std::string& game_id, std::chrono::millise { std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; + u64 new_time; - std::lock_guard guard(m_mutex); - m_time_list->Get(filtered_game_id, &previous_time); - m_time_list->Set(filtered_game_id, previous_time + static_cast(time_emulated.count())); - m_ini.Save(m_ini_path); + { + std::lock_guard guard(m_mutex); + + m_time_list->Get(filtered_game_id, &previous_time); + new_time = previous_time + static_cast(time_emulated.count()); + m_time_list->Set(filtered_game_id, new_time); + m_ini.Save(m_ini_path); + } + + UpdateEvent::Trigger(filtered_game_id, static_cast(new_time)); } std::chrono::milliseconds TimePlayedManager::GetTimePlayed(const std::string& game_id) const @@ -46,10 +55,3 @@ std::chrono::milliseconds TimePlayedManager::GetTimePlayed(const std::string& ga m_time_list->Get(filtered_game_id, &previous_time); return std::chrono::milliseconds(previous_time); } - -void TimePlayedManager::Reload() -{ - std::lock_guard guard(m_mutex); - m_ini.Load(m_ini_path); - m_time_list = m_ini.GetOrCreateSection("TimePlayed"); -} diff --git a/Source/Core/Core/TimePlayed.h b/Source/Core/Core/TimePlayed.h index 6adfc56348..2c629fc31b 100644 --- a/Source/Core/Core/TimePlayed.h +++ b/Source/Core/Core/TimePlayed.h @@ -8,6 +8,7 @@ #include #include "Common/CommonTypes.h" +#include "Common/HookableEvent.h" #include "Common/IniFile.h" class TimePlayedManager @@ -26,7 +27,8 @@ public: std::chrono::milliseconds GetTimePlayed(const std::string& game_id) const; - void Reload(); + using UpdateEvent = + Common::HookableEvent<"Time Played Update", const std::string&, std::chrono::milliseconds>; private: TimePlayedManager(); diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index 9ff39f90ed..e0961c47ba 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -3,18 +3,21 @@ #include "DolphinQt/GameList/GameListModel.h" +#include +#include + #include #include #include #include #include "Core/Config/MainSettings.h" -#include "Core/Core.h" #include "Core/TimePlayed.h" #include "DiscIO/Enums.h" #include "DolphinQt/QtUtils/ImageConverter.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/Resources.h" #include "DolphinQt/Settings.h" @@ -35,8 +38,6 @@ GameListModel::GameListModel(QObject* parent) &GameTracker::RefreshAll); connect(&Settings::Instance(), &Settings::TitleDBReloadRequested, [this] { m_title_database = Core::TitleDatabase(); }); - connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, - &GameListModel::OnEmulationStateChanged); for (const QString& dir : Settings::Instance().GetPaths()) m_tracker.AddDirectory(dir); @@ -50,6 +51,28 @@ GameListModel::GameListModel(QObject* parent) emit layoutChanged(); }); + const auto on_time_played_update = [this](const std::string& game_id, + const std::chrono::milliseconds) { + const auto update_cell = [this, game_id]() { + for (int model_row = 0; model_row < m_games.size(); ++model_row) + { + if (game_id != m_games[model_row]->GetGameID()) + continue; + + const QModelIndex time_played_index = + index(model_row, static_cast(Column::TimePlayed)); + emit dataChanged(time_played_index, time_played_index); + + // Multiple entries in the GameList can have the same GameID, so don't break out of the + // loop when a match is found. + } + }; + QueueOnObject(this, update_cell); + }; + + m_time_played_update_event = + TimePlayedManager::UpdateEvent::Register(on_time_played_update, "GameListModel"); + auto& settings = Settings::GetQSettings(); m_tag_list = settings.value(QStringLiteral("gamelist/tags")).toStringList(); @@ -508,11 +531,3 @@ void GameListModel::PurgeCache() { m_tracker.PurgeCache(); } - -void GameListModel::OnEmulationStateChanged(Core::State state) -{ - if (state == Core::State::Uninitialized) - { - m_time_played_manager.Reload(); - } -} diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index f513a1dde1..f4213a0708 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -12,7 +12,6 @@ #include #include -#include "Core/Core.h" #include "Core/TimePlayed.h" #include "Core/TitleDatabase.h" @@ -90,8 +89,6 @@ private: // Index in m_games, or -1 if it isn't found int FindGameIndex(const std::string& path) const; - void OnEmulationStateChanged(Core::State state); - QStringList m_tag_list; QMap m_game_tags; @@ -99,6 +96,7 @@ private: QList> m_games; Core::TitleDatabase m_title_database; TimePlayedManager& m_time_played_manager; + Common::EventHook m_time_played_update_event; QString m_term; float m_scale = 1.0; }; From ad0fe22f5c44fd4779720fd5cd9c6bd9210b8381 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sat, 15 Mar 2025 13:42:53 -0700 Subject: [PATCH 3/7] CPUManager: Get TimePlayedManager reference outside loop --- Source/Core/Core/HW/CPU.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 832162c138..55806a4b20 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -75,10 +75,10 @@ void CPUManager::StartTimePlayedTimer() // Steady clock for greater accuracy of timing std::chrono::steady_clock timer; auto prev_time = timer.now(); + auto& time_played_manager = TimePlayedManager::GetInstance(); while (true) { - auto& time_played_manager = TimePlayedManager::GetInstance(); auto curr_time = timer.now(); // Check that emulation is not paused From c4009418ef990701a062e523ea590aef9410f056 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sat, 10 May 2025 14:58:07 -0700 Subject: [PATCH 4/7] TimePlayedManager: Fix logic for adding time slice Fix various edge cases that could cause the wrong amount of time to be added per time slice. In particular: * When a game was running, pausing (or framestepping/breaking) during a given slice and then unpausing during that same slice would add the entire elapsed time, regardless of what fraction of it was spent paused. * Stopping a game during a slice would discard that segment of elapsed time. * Pausing a game without resuming it during the same slice would discard all of the time in the slice. --- Source/Core/Core/HW/CPU.cpp | 38 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 55806a4b20..02ab0dcc1f 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -77,31 +77,24 @@ void CPUManager::StartTimePlayedTimer() auto prev_time = timer.now(); auto& time_played_manager = TimePlayedManager::GetInstance(); - while (true) + while (m_state != State::PowerDown) { + m_time_played_finish_sync.WaitFor(std::chrono::seconds(30)); auto curr_time = timer.now(); - // Check that emulation is not paused - // If the emulation is paused, wait for SetStepping() to reactivate - if (m_state == State::Running) - { - const std::string game_id = SConfig::GetInstance().GetGameID(); - const auto diff_time = - std::chrono::duration_cast(curr_time - prev_time); - time_played_manager.AddTime(game_id, diff_time); - } - else if (m_state == State::Stepping) + const std::string game_id = SConfig::GetInstance().GetGameID(); + const auto diff_time = + std::chrono::duration_cast(curr_time - prev_time); + time_played_manager.AddTime(game_id, diff_time); + + // If the emulation is paused, wait for SetStateLocked() to reactivate + if (m_state == State::Stepping) { m_time_played_finish_sync.Wait(); curr_time = timer.now(); } prev_time = curr_time; - - if (m_state == State::PowerDown) - return; - - m_time_played_finish_sync.WaitFor(std::chrono::seconds(30)); } } @@ -299,7 +292,17 @@ bool CPUManager::SetStateLocked(State s) return false; if (s == State::Stepping) m_system.GetPowerPC().GetBreakPoints().ClearTemporary(); - m_state = s; + + // CPUThreadGuard is used in various places to avoid racing with the CPU thread. CPUThreadGuard + // can indirectly call SetStateLocked, which can result in it getting called with the same state + // that m_state already had. Since m_time_played_finish_sync only needs to be Set when m_state + // changes, avoid doing so when it hasn't. + if (m_state != s) + { + m_state = s; + m_time_played_finish_sync.Set(); + } + return true; } @@ -322,7 +325,6 @@ void CPUManager::SetStepping(bool stepping) else if (SetStateLocked(State::Running)) { m_state_cpu_cvar.notify_one(); - m_time_played_finish_sync.Set(); RunAdjacentSystems(true); } } From b7ebd68cdd02c6c3bf28e15bf4308a0ed98702d2 Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sat, 15 Mar 2025 13:38:54 -0700 Subject: [PATCH 5/7] TimePlayedManager: Add some const --- Source/Core/Core/HW/CPU.cpp | 4 ++-- Source/Core/Core/TimePlayed.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 02ab0dcc1f..6b76e3199c 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -286,7 +286,7 @@ void CPUManager::StepOpcode(Common::Event* event) } // Requires m_state_change_lock -bool CPUManager::SetStateLocked(State s) +bool CPUManager::SetStateLocked(const State s) { if (m_state == State::PowerDown) return false; @@ -306,7 +306,7 @@ bool CPUManager::SetStateLocked(State s) return true; } -void CPUManager::SetStepping(bool stepping) +void CPUManager::SetStepping(const bool stepping) { std::lock_guard stepping_lock(m_stepping_lock); std::unique_lock state_lock(m_state_change_lock); diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp index f59ad5c702..223b9390c9 100644 --- a/Source/Core/Core/TimePlayed.cpp +++ b/Source/Core/Core/TimePlayed.cpp @@ -28,9 +28,10 @@ TimePlayedManager& TimePlayedManager::GetInstance() return time_played_manager; } -void TimePlayedManager::AddTime(const std::string& game_id, std::chrono::milliseconds time_emulated) +void TimePlayedManager::AddTime(const std::string& game_id, + const std::chrono::milliseconds time_emulated) { - std::string filtered_game_id = Common::EscapeFileName(game_id); + const std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; u64 new_time; @@ -48,7 +49,7 @@ void TimePlayedManager::AddTime(const std::string& game_id, std::chrono::millise std::chrono::milliseconds TimePlayedManager::GetTimePlayed(const std::string& game_id) const { - std::string filtered_game_id = Common::EscapeFileName(game_id); + const std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; std::lock_guard guard(m_mutex); From f14ac36ac894a0ec3198c799574b9344946683ca Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Mon, 17 Mar 2025 19:14:41 -0700 Subject: [PATCH 6/7] REVIEW COMMIT Update every 10 seconds and display seconds in GameList to make it easier to test the timing changes. Will delete this commit before merging. --- Source/Core/Core/HW/CPU.cpp | 2 +- Source/Core/DolphinQt/GameList/GameListModel.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 6b76e3199c..08eda74e9f 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -79,7 +79,7 @@ void CPUManager::StartTimePlayedTimer() while (m_state != State::PowerDown) { - m_time_played_finish_sync.WaitFor(std::chrono::seconds(30)); + m_time_played_finish_sync.WaitFor(std::chrono::seconds(10)); auto curr_time = timer.now(); const std::string game_id = SConfig::GetInstance().GetGameID(); diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index e0961c47ba..6d2363e0ed 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -222,10 +222,13 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const const std::chrono::milliseconds total_time = m_time_played_manager.GetTimePlayed(game_id); const auto total_minutes = std::chrono::duration_cast(total_time); const auto total_hours = std::chrono::duration_cast(total_time); + const auto total_seconds = std::chrono::duration_cast(total_time); // i18n: A time displayed as hours and minutes - QString formatted_time = - tr("%1h %2m").arg(total_hours.count()).arg(total_minutes.count() % 60); + QString formatted_time = tr("%1h %2m %3s") + .arg(total_hours.count()) + .arg(total_minutes.count() % 60) + .arg(total_seconds.count() % 60); return formatted_time; } if (role == SORT_ROLE) From 9a3825c62257c8f3d44b0994cb33c6d1944b886e Mon Sep 17 00:00:00 2001 From: Dentomologist Date: Sun, 18 May 2025 16:27:54 -0700 Subject: [PATCH 7/7] InfoWidget: Add Time Played display and editing Add a section showing the game's time played and allowing the user to edit the values via the QSpinBoxes displaying them. --- Source/Core/Core/TimePlayed.cpp | 27 ++++++- Source/Core/Core/TimePlayed.h | 4 + Source/Core/DolphinQt/Config/InfoWidget.cpp | 90 ++++++++++++++++++++- Source/Core/DolphinQt/Config/InfoWidget.h | 8 ++ 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp index 223b9390c9..4906f4f35f 100644 --- a/Source/Core/Core/TimePlayed.cpp +++ b/Source/Core/Core/TimePlayed.cpp @@ -13,6 +13,9 @@ #include "Common/IniFile.h" #include "Common/NandPaths.h" +static constexpr std::chrono::milliseconds MAX_TIME_PLAYED = + std::chrono::hours(TimePlayedManager::MAX_HOURS) + std::chrono::minutes(59); + TimePlayedManager::TimePlayedManager() : m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") { @@ -33,18 +36,34 @@ void TimePlayedManager::AddTime(const std::string& game_id, { const std::string filtered_game_id = Common::EscapeFileName(game_id); u64 previous_time; - u64 new_time; + std::chrono::milliseconds capped_new_time; { std::lock_guard guard(m_mutex); m_time_list->Get(filtered_game_id, &previous_time); - new_time = previous_time + static_cast(time_emulated.count()); - m_time_list->Set(filtered_game_id, new_time); + const auto new_time = std::chrono::milliseconds(previous_time) + time_emulated; + capped_new_time = std::min(MAX_TIME_PLAYED, new_time); + m_time_list->Set(filtered_game_id, static_cast(capped_new_time.count())); m_ini.Save(m_ini_path); } - UpdateEvent::Trigger(filtered_game_id, static_cast(new_time)); + UpdateEvent::Trigger(filtered_game_id, capped_new_time); +} + +void TimePlayedManager::SetTimePlayed(const std::string& game_id, + const std::chrono::milliseconds time_played) +{ + const std::string filtered_game_id = Common::EscapeFileName(game_id); + const std::chrono::milliseconds capped_time_played = std::min(MAX_TIME_PLAYED, time_played); + + { + std::lock_guard guard(m_mutex); + m_time_list->Set(filtered_game_id, static_cast(capped_time_played.count())); + m_ini.Save(m_ini_path); + } + + UpdateEvent::Trigger(filtered_game_id, capped_time_played); } std::chrono::milliseconds TimePlayedManager::GetTimePlayed(const std::string& game_id) const diff --git a/Source/Core/Core/TimePlayed.h b/Source/Core/Core/TimePlayed.h index 2c629fc31b..8ae9300330 100644 --- a/Source/Core/Core/TimePlayed.h +++ b/Source/Core/Core/TimePlayed.h @@ -25,11 +25,15 @@ public: void AddTime(const std::string& game_id, std::chrono::milliseconds time_emulated); + void SetTimePlayed(const std::string& game_id, std::chrono::milliseconds time_played); + std::chrono::milliseconds GetTimePlayed(const std::string& game_id) const; using UpdateEvent = Common::HookableEvent<"Time Played Update", const std::string&, std::chrono::milliseconds>; + static constexpr int MAX_HOURS = 999'999; + private: TimePlayedManager(); diff --git a/Source/Core/DolphinQt/Config/InfoWidget.cpp b/Source/Core/DolphinQt/Config/InfoWidget.cpp index 81371f0caf..0ed0b52b44 100644 --- a/Source/Core/DolphinQt/Config/InfoWidget.cpp +++ b/Source/Core/DolphinQt/Config/InfoWidget.cpp @@ -3,17 +3,25 @@ #include "DolphinQt/Config/InfoWidget.h" +#include + #include #include #include #include #include +#include #include #include #include +#include +#include #include +#include "Common/HookableEvent.h" + #include "Core/ConfigManager.h" +#include "Core/TimePlayed.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" @@ -22,6 +30,7 @@ #include "DolphinQt/QtUtils/DolphinFileDialog.h" #include "DolphinQt/QtUtils/ImageConverter.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" #include "UICommon/UICommon.h" @@ -37,10 +46,17 @@ InfoWidget::InfoWidget(const UICommon::GameFile& game) : m_game(game) if (!game.GetLanguages().empty()) layout->addWidget(CreateBannerDetails()); + layout->addWidget(CreateTimePlayedDetails()); + + layout->addStretch(1); + setLayout(layout); } -InfoWidget::~InfoWidget() = default; +InfoWidget::~InfoWidget() +{ + m_time_played_update_event.reset(); +} QGroupBox* InfoWidget::CreateFileDetails() { @@ -181,6 +197,78 @@ QGroupBox* InfoWidget::CreateBannerDetails() return group; } +QGroupBox* InfoWidget::CreateTimePlayedDetails() +{ + const auto set_time_played = [this]() { + const int hours = m_hours_played->text().toInt(); + const int minutes = m_minutes_played->text().toInt(); + const std::chrono::milliseconds time_played = + std::chrono::hours(hours) + std::chrono::minutes(minutes); + TimePlayedManager::GetInstance().SetTimePlayed(m_game.GetGameID(), time_played); + }; + + auto* const time_played_label = new QLabel(tr("Time Played:")); + + m_hours_played = new QSpinBox; + m_hours_played->setRange(0, TimePlayedManager::MAX_HOURS); + connect(m_hours_played, &QSpinBox::valueChanged, this, set_time_played); + + m_minutes_played = new QSpinBox; + m_minutes_played->setRange(0, 59); + connect(m_minutes_played, &QSpinBox::valueChanged, this, set_time_played); + + auto* const hours_label = new QLabel(tr("Hours")); + hours_label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + auto* const minutes_label = new QLabel(tr("Minutes")); + minutes_label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + auto* const time_played_layout = new QHBoxLayout; + time_played_layout->addWidget(m_hours_played); + time_played_layout->addWidget(hours_label); + time_played_layout->addWidget(m_minutes_played); + time_played_layout->addWidget(minutes_label); + + auto* const layout = new QFormLayout; + layout->addRow(time_played_label, time_played_layout); + + const auto update_time_played = [this](const std::string& game_id, + const std::chrono::milliseconds time_played) { + if (game_id != m_game.GetGameID()) + return; + + using namespace std::chrono_literals; + + const auto hours_played = time_played / 1h; + const auto minutes_played = (time_played % 60min) / 1min; + + QSignalBlocker hours_blocker(m_hours_played); + QSignalBlocker minutes_blocker(m_minutes_played); + + m_hours_played->setValue(hours_played); + m_minutes_played->setValue(minutes_played); + }; + + const std::chrono::milliseconds time_played = + TimePlayedManager::GetInstance().GetTimePlayed(m_game.GetGameID()); + update_time_played(m_game.GetGameID(), time_played); + + const auto update_time_played_on_host_thread = + [this, update_time_played](const std::string& game_id, + const std::chrono::milliseconds time_played) { + QueueOnObject(this, [update_time_played, game_id, time_played]() { + update_time_played(game_id, time_played); + }); + }; + m_time_played_update_event = + TimePlayedManager::UpdateEvent::Register(update_time_played_on_host_thread, "InfoWidget"); + + auto* const group_box = new QGroupBox(tr("Time Played Details")); + group_box->setLayout(layout); + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + return group_box; +} + QWidget* InfoWidget::CreateBannerGraphic(const QPixmap& image) { QWidget* widget = new QWidget(); diff --git a/Source/Core/DolphinQt/Config/InfoWidget.h b/Source/Core/DolphinQt/Config/InfoWidget.h index c9493f1276..1905703d8e 100644 --- a/Source/Core/DolphinQt/Config/InfoWidget.h +++ b/Source/Core/DolphinQt/Config/InfoWidget.h @@ -8,6 +8,8 @@ #include +#include "Common/HookableEvent.h" + #include "UICommon/GameFile.h" namespace DiscIO @@ -19,6 +21,7 @@ class QComboBox; class QGroupBox; class QLineEdit; class QPixmap; +class QSpinBox; class QTextEdit; class InfoWidget final : public QWidget @@ -35,6 +38,7 @@ private: QGroupBox* CreateFileDetails(); QGroupBox* CreateGameDetails(); QGroupBox* CreateBannerDetails(); + QGroupBox* CreateTimePlayedDetails(); QLineEdit* CreateValueDisplay(const QString& value); QLineEdit* CreateValueDisplay(const std::string& value = ""); void CreateLanguageSelector(); @@ -46,4 +50,8 @@ private: QLineEdit* m_name = {}; QLineEdit* m_maker = {}; QTextEdit* m_description = {}; + QSpinBox* m_hours_played = {}; + QSpinBox* m_minutes_played = {}; + + Common::EventHook m_time_played_update_event; };