summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdmiral H. Curtiss <pikachu025@gmail.com>2025-02-16 23:37:13 +0100
committerGitHub <noreply@github.com>2025-02-16 23:37:13 +0100
commitf0edcb2cfb74e86bb1adefa0c5650177e67c4330 (patch)
tree007fd7dd4e761db3ffc70f2ffece0d1369f2cd43
parent5acbdf730a44d00b46528833334f82f44a6a6e67 (diff)
parentfffb499da2fdde53e1ff25e17f2f718cd608167f (diff)
Merge pull request #11469 from aminoa/master
Track Time Played (Core and QT)
-rw-r--r--Source/Core/Core/CMakeLists.txt2
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp3
-rw-r--r--Source/Core/Core/Config/MainSettings.h2
-rw-r--r--Source/Core/Core/ConfigManager.cpp42
-rw-r--r--Source/Core/Core/ConfigManager.h15
-rw-r--r--Source/Core/Core/HW/CPU.cpp54
-rw-r--r--Source/Core/Core/HW/CPU.h4
-rw-r--r--Source/Core/Core/TimePlayed.cpp66
-rw-r--r--Source/Core/Core/TimePlayed.h40
-rw-r--r--Source/Core/DolphinLib.props2
-rw-r--r--Source/Core/DolphinQt/GameList/GameList.cpp3
-rw-r--r--Source/Core/DolphinQt/GameList/GameListModel.cpp33
-rw-r--r--Source/Core/DolphinQt/GameList/GameListModel.h6
-rw-r--r--Source/Core/DolphinQt/MenuBar.cpp1
-rw-r--r--Source/Core/DolphinQt/Settings/InterfacePane.cpp21
-rw-r--r--Source/Core/DolphinQt/Settings/InterfacePane.h8
16 files changed, 296 insertions, 6 deletions
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 33a97d05e7..b4437747e8 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -539,6 +539,8 @@ add_library(core
SysConf.h
System.cpp
System.h
+ TimePlayed.cpp
+ TimePlayed.h
TitleDatabase.cpp
TitleDatabase.h
WC24PatchEngine.cpp
diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp
index 0442347fdd..4159042b2f 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -309,6 +309,7 @@ const Info<int> MAIN_GDB_PORT{{System::Main, "General", "GDBPort"}, -1};
const Info<int> MAIN_ISO_PATH_COUNT{{System::Main, "General", "ISOPaths"}, 0};
const Info<std::string> MAIN_SKYLANDERS_PATH{{System::Main, "General", "SkylandersCollectionPath"},
""};
+const Info<bool> MAIN_TIME_TRACKING{{System::Main, "General", "EnablePlayTimeTracking"}, true};
static Info<std::string> MakeISOPathConfigInfo(size_t idx)
{
@@ -459,6 +460,8 @@ const Info<bool> MAIN_GAMELIST_COLUMN_BLOCK_SIZE{{System::Main, "GameList", "Col
false};
const Info<bool> MAIN_GAMELIST_COLUMN_COMPRESSION{{System::Main, "GameList", "ColumnCompression"},
false};
+const Info<bool> MAIN_GAMELIST_COLUMN_TIME_PLAYED{{System::Main, "GameList", "ColumnTimePlayed"},
+ true};
const Info<bool> MAIN_GAMELIST_COLUMN_TAGS{{System::Main, "GameList", "ColumnTags"}, false};
// Main.FifoPlayer
diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h
index b6e8f966c7..8764a5d91c 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -186,6 +186,7 @@ extern const Info<int> MAIN_RENDER_WINDOW_HEIGHT;
extern const Info<bool> MAIN_RENDER_WINDOW_AUTOSIZE;
extern const Info<bool> MAIN_KEEP_WINDOW_ON_TOP;
extern const Info<bool> MAIN_DISABLE_SCREENSAVER;
+extern const Info<bool> MAIN_TIME_TRACKING;
// Main.General
@@ -295,6 +296,7 @@ extern const Info<bool> MAIN_GAMELIST_COLUMN_FILE_SIZE;
extern const Info<bool> MAIN_GAMELIST_COLUMN_FILE_FORMAT;
extern const Info<bool> MAIN_GAMELIST_COLUMN_BLOCK_SIZE;
extern const Info<bool> MAIN_GAMELIST_COLUMN_COMPRESSION;
+extern const Info<bool> MAIN_GAMELIST_COLUMN_TIME_PLAYED;
extern const Info<bool> MAIN_GAMELIST_COLUMN_TAGS;
// Main.FifoPlayer
diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp
index 21d722290e..5a123c87cd 100644
--- a/Source/Core/Core/ConfigManager.cpp
+++ b/Source/Core/Core/ConfigManager.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
#include <climits>
#include <memory>
+#include <mutex>
#include <optional>
#include <sstream>
#include <string>
@@ -98,14 +99,52 @@ void SConfig::LoadSettings()
Config::Load();
}
+const std::string SConfig::GetGameID() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_game_id;
+}
+
+const std::string SConfig::GetGameTDBID() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_gametdb_id;
+}
+
+const std::string SConfig::GetTitleName() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_title_name;
+}
+
+const std::string SConfig::GetTitleDescription() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_title_description;
+}
+
+u64 SConfig::GetTitleID() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_title_id;
+}
+
+u16 SConfig::GetRevision() const
+{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
+ return m_revision;
+}
+
void SConfig::ResetRunningGameMetadata()
{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
const DiscIO::Partition& partition)
{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
if (partition == volume.GetGamePartition())
{
SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(),
@@ -122,6 +161,7 @@ void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform)
{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
const u64 tmd_title_id = tmd.GetTitleId();
// If we're launching a disc game, we want to read the revision from
@@ -139,12 +179,14 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat
void SConfig::SetRunningGameMetadata(const std::string& game_id)
{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id,
u64 title_id, u16 revision, DiscIO::Region region)
{
+ std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id ||
m_title_id != title_id || m_revision != revision;
m_game_id = game_id;
diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h
index 999cc9f86c..d21b087a51 100644
--- a/Source/Core/Core/ConfigManager.h
+++ b/Source/Core/Core/ConfigManager.h
@@ -4,6 +4,7 @@
#pragma once
#include <limits>
+#include <mutex>
#include <optional>
#include <set>
#include <string>
@@ -58,15 +59,16 @@ struct SConfig
std::string m_strSRAM;
std::string m_debugger_game_id;
+
// TODO: remove this as soon as the ticket view hack in IOS/ES/Views is dropped.
bool m_disc_booted_from_game_list = false;
- const std::string& GetGameID() const { return m_game_id; }
- const std::string& GetGameTDBID() const { return m_gametdb_id; }
- const std::string& GetTitleName() const { return m_title_name; }
- const std::string& GetTitleDescription() const { return m_title_description; }
- u64 GetTitleID() const { return m_title_id; }
- u16 GetRevision() const { return m_revision; }
+ const std::string GetGameID() const;
+ const std::string GetGameTDBID() const;
+ const std::string GetTitleName() const;
+ const std::string GetTitleDescription() const;
+ u64 GetTitleID() const;
+ u16 GetRevision() const;
void ResetRunningGameMetadata();
void SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition);
void SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform);
@@ -114,6 +116,7 @@ private:
u64 title_id, u16 revision, DiscIO::Region region);
static SConfig* m_Instance;
+ mutable std::recursive_mutex m_metadata_lock;
std::string m_game_id;
std::string m_gametdb_id;
diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp
index 1eae912476..487a33c6ff 100644
--- a/Source/Core/Core/HW/CPU.cpp
+++ b/Source/Core/Core/HW/CPU.cpp
@@ -10,12 +10,16 @@
#include "AudioCommon/AudioCommon.h"
#include "Common/CommonTypes.h"
#include "Common/Event.h"
+#include "Common/Timer.h"
#include "Core/CPUThreadConfigCallback.h"
+#include "Core/Config/MainSettings.h"
+#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/Host.h"
#include "Core/PowerPC/GDBStub.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
+#include "Core/TimePlayed.h"
#include "VideoCommon/Fifo.h"
namespace CPU
@@ -63,6 +67,41 @@ void CPUManager::ExecutePendingJobs(std::unique_lock<std::mutex>& state_lock)
}
}
+void CPUManager::StartTimePlayedTimer()
+{
+ // Steady clock for greater accuracy of timing
+ std::chrono::steady_clock timer;
+ auto prev_time = timer.now();
+
+ while (true)
+ {
+ const std::string game_id = SConfig::GetInstance().GetGameID();
+ TimePlayed time_played(game_id);
+ 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 auto diff_time =
+ std::chrono::duration_cast<std::chrono::milliseconds>(curr_time - prev_time);
+ time_played.AddTime(diff_time);
+ }
+ else 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));
+ }
+}
+
void CPUManager::Run()
{
auto& power_pc = m_system.GetPowerPC();
@@ -71,6 +110,13 @@ void CPUManager::Run()
// We can't rely on PowerPC::Init doing it, since it's called from EmuThread.
PowerPC::RoundingModeUpdated(power_pc.GetPPCState());
+ // Start a separate time tracker thread
+ std::thread timing;
+ if (Config::Get(Config::MAIN_TIME_TRACKING))
+ {
+ timing = std::thread(&CPUManager::StartTimePlayedTimer, this);
+ }
+
std::unique_lock state_lock(m_state_change_lock);
while (m_state != State::PowerDown)
{
@@ -165,6 +211,13 @@ void CPUManager::Run()
break;
}
}
+
+ if (timing.joinable())
+ {
+ m_time_played_finish_sync.Set();
+ timing.join();
+ }
+
state_lock.unlock();
Host_UpdateDisasmDialog();
}
@@ -266,6 +319,7 @@ void CPUManager::SetStepping(bool stepping)
else if (SetStateLocked(State::Running))
{
m_state_cpu_cvar.notify_one();
+ m_time_played_finish_sync.Set();
RunAdjacentSystems(true);
}
}
diff --git a/Source/Core/Core/HW/CPU.h b/Source/Core/Core/HW/CPU.h
index 57e8a31a29..b73471136d 100644
--- a/Source/Core/Core/HW/CPU.h
+++ b/Source/Core/Core/HW/CPU.h
@@ -8,6 +8,8 @@
#include <mutex>
#include <queue>
+#include "Common/Event.h"
+
namespace Common
{
class Event;
@@ -102,6 +104,7 @@ public:
private:
void FlushStepSyncEventLocked();
void ExecutePendingJobs(std::unique_lock<std::mutex>& state_lock);
+ void StartTimePlayedTimer();
void RunAdjacentSystems(bool running);
bool SetStateLocked(State s);
@@ -133,6 +136,7 @@ private:
bool m_state_cpu_step_instruction = false;
Common::Event* m_state_cpu_step_instruction_sync = nullptr;
std::queue<std::function<void()>> m_pending_jobs;
+ Common::Event m_time_played_finish_sync;
Core::System& m_system;
};
diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp
new file mode 100644
index 0000000000..e20a0c014e
--- /dev/null
+++ b/Source/Core/Core/TimePlayed.cpp
@@ -0,0 +1,66 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/TimePlayed.h"
+
+#include <chrono>
+#include <string>
+
+#include "Common/CommonTypes.h"
+#include "Common/FileUtil.h"
+#include "Common/IniFile.h"
+#include "Common/NandPaths.h"
+
+TimePlayed::TimePlayed()
+ : m_game_id(""), m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini")
+{
+ Reload();
+}
+
+TimePlayed::TimePlayed(std::string game_id)
+ : m_game_id(Common::EscapeFileName(game_id)), // filter for unsafe characters
+ m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini")
+{
+ Reload();
+}
+
+TimePlayed::~TimePlayed() = default;
+
+void TimePlayed::AddTime(std::chrono::milliseconds time_emulated)
+{
+ if (m_game_id == "")
+ {
+ return;
+ }
+
+ u64 previous_time;
+ m_time_list->Get(m_game_id, &previous_time);
+ m_time_list->Set(m_game_id, previous_time + static_cast<u64>(time_emulated.count()));
+ m_ini.Save(m_ini_path);
+}
+
+std::chrono::milliseconds TimePlayed::GetTimePlayed() const
+{
+ if (m_game_id == "")
+ {
+ return std::chrono::milliseconds(0);
+ }
+
+ u64 previous_time;
+ m_time_list->Get(m_game_id, &previous_time);
+ return std::chrono::milliseconds(previous_time);
+}
+
+std::chrono::milliseconds TimePlayed::GetTimePlayed(std::string game_id) const
+{
+ std::string filtered_game_id = Common::EscapeFileName(game_id);
+ u64 previous_time;
+ m_time_list->Get(filtered_game_id, &previous_time);
+ return std::chrono::milliseconds(previous_time);
+}
+
+void TimePlayed::Reload()
+{
+ 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
new file mode 100644
index 0000000000..306438b778
--- /dev/null
+++ b/Source/Core/Core/TimePlayed.h
@@ -0,0 +1,40 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <chrono>
+#include <string>
+
+#include "Common/CommonTypes.h"
+#include "Common/IniFile.h"
+
+class TimePlayed
+{
+public:
+ // used for QT interface - general access to time played for games
+ TimePlayed();
+
+ TimePlayed(std::string game_id);
+
+ // 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;
+
+ ~TimePlayed();
+
+ void AddTime(std::chrono::milliseconds time_emulated);
+
+ std::chrono::milliseconds GetTimePlayed() const;
+ std::chrono::milliseconds GetTimePlayed(std::string game_id) const;
+
+ void Reload();
+
+private:
+ std::string m_game_id;
+ std::string m_ini_path;
+ Common::IniFile m_ini;
+ Common::IniFile::Section* m_time_list;
+};
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index e41e9fc26a..367c347213 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -459,6 +459,7 @@
<ClInclude Include="Core\SyncIdentifier.h" />
<ClInclude Include="Core\SysConf.h" />
<ClInclude Include="Core\System.h" />
+ <ClInclude Include="Core\TimePlayed.h" />
<ClInclude Include="Core\TitleDatabase.h" />
<ClInclude Include="Core\WC24PatchEngine.h" />
<ClInclude Include="Core\WiiRoot.h" />
@@ -1125,6 +1126,7 @@
<ClCompile Include="Core\State.cpp" />
<ClCompile Include="Core\SysConf.cpp" />
<ClCompile Include="Core\System.cpp" />
+ <ClCompile Include="Core\TimePlayed.cpp" />
<ClCompile Include="Core\TitleDatabase.cpp" />
<ClCompile Include="Core\WiiRoot.cpp" />
<ClCompile Include="Core\WiiUtils.cpp" />
diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp
index 7824925396..a4514a0449 100644
--- a/Source/Core/DolphinQt/GameList/GameList.cpp
+++ b/Source/Core/DolphinQt/GameList/GameList.cpp
@@ -208,6 +208,7 @@ void GameList::MakeListView()
SetResizeMode(Column::FileFormat, Mode::Fixed);
SetResizeMode(Column::BlockSize, Mode::Fixed);
SetResizeMode(Column::Compression, Mode::Fixed);
+ SetResizeMode(Column::TimePlayed, Mode::Interactive);
SetResizeMode(Column::Tags, Mode::Interactive);
// Cells have 3 pixels of padding, so the width of these needs to be image width + 6. Banners
@@ -273,6 +274,7 @@ void GameList::UpdateColumnVisibility()
SetVisiblity(Column::FileFormat, Config::Get(Config::MAIN_GAMELIST_COLUMN_FILE_FORMAT));
SetVisiblity(Column::BlockSize, Config::Get(Config::MAIN_GAMELIST_COLUMN_BLOCK_SIZE));
SetVisiblity(Column::Compression, Config::Get(Config::MAIN_GAMELIST_COLUMN_COMPRESSION));
+ SetVisiblity(Column::TimePlayed, Config::Get(Config::MAIN_GAMELIST_COLUMN_TIME_PLAYED));
SetVisiblity(Column::Tags, Config::Get(Config::MAIN_GAMELIST_COLUMN_TAGS));
}
@@ -1005,6 +1007,7 @@ void GameList::OnColumnVisibilityToggled(const QString& row, bool visible)
{tr("File Format"), Column::FileFormat},
{tr("Block Size"), Column::BlockSize},
{tr("Compression"), Column::Compression},
+ {tr("Time Played"), Column::TimePlayed},
{tr("Tags"), Column::Tags},
};
diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp
index c70a870c23..c8c1d8b165 100644
--- a/Source/Core/DolphinQt/GameList/GameListModel.cpp
+++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp
@@ -9,6 +9,8 @@
#include <QRegularExpression>
#include "Core/Config/MainSettings.h"
+#include "Core/Core.h"
+#include "Core/TimePlayed.h"
#include "DiscIO/Enums.h"
@@ -32,6 +34,8 @@ GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(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);
@@ -187,6 +191,25 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
return compression.isEmpty() ? tr("No Compression") : compression;
}
break;
+ case Column::TimePlayed:
+ if (role == Qt::DisplayRole)
+ {
+ const std::string game_id = game.GetGameID();
+ const std::chrono::milliseconds total_time = m_timer.GetTimePlayed(game_id);
+ const auto total_minutes = std::chrono::duration_cast<std::chrono::minutes>(total_time);
+ const auto total_hours = std::chrono::duration_cast<std::chrono::hours>(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);
+ return formatted_time;
+ }
+ if (role == SORT_ROLE)
+ {
+ const std::string game_id = game.GetGameID();
+ return static_cast<qlonglong>(m_timer.GetTimePlayed(game_id).count());
+ }
+ break;
case Column::Tags:
if (role == Qt::DisplayRole || role == SORT_ROLE)
{
@@ -232,6 +255,8 @@ QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int
return tr("Block Size");
case Column::Compression:
return tr("Compression");
+ case Column::TimePlayed:
+ return tr("Time Played");
case Column::Tags:
return tr("Tags");
default:
@@ -480,3 +505,11 @@ void GameListModel::PurgeCache()
{
m_tracker.PurgeCache();
}
+
+void GameListModel::OnEmulationStateChanged(Core::State state)
+{
+ if (state == Core::State::Uninitialized)
+ {
+ m_timer.Reload();
+ }
+}
diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h
index b76e0a37c2..baac5d507d 100644
--- a/Source/Core/DolphinQt/GameList/GameListModel.h
+++ b/Source/Core/DolphinQt/GameList/GameListModel.h
@@ -12,6 +12,8 @@
#include <QStringList>
#include <QVariant>
+#include "Core/Core.h"
+#include "Core/TimePlayed.h"
#include "Core/TitleDatabase.h"
#include "DolphinQt/GameList/GameTracker.h"
@@ -58,6 +60,7 @@ public:
FileFormat,
BlockSize,
Compression,
+ TimePlayed,
Tags,
Count,
};
@@ -87,12 +90,15 @@ 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<QString, QVariant> m_game_tags;
GameTracker m_tracker;
QList<std::shared_ptr<const UICommon::GameFile>> m_games;
Core::TitleDatabase m_title_database;
+ TimePlayed m_timer;
QString m_term;
float m_scale = 1.0;
};
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index fef441c062..24c6cc2d04 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -701,6 +701,7 @@ void MenuBar::AddListColumnsMenu(QMenu* view_menu)
{tr("File Format"), &Config::MAIN_GAMELIST_COLUMN_FILE_FORMAT},
{tr("Block Size"), &Config::MAIN_GAMELIST_COLUMN_BLOCK_SIZE},
{tr("Compression"), &Config::MAIN_GAMELIST_COLUMN_COMPRESSION},
+ {tr("Time Played"), &Config::MAIN_GAMELIST_COLUMN_TIME_PLAYED},
{tr("Tags"), &Config::MAIN_GAMELIST_COLUMN_TAGS}};
QActionGroup* column_group = new QActionGroup(this);
diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.cpp b/Source/Core/DolphinQt/Settings/InterfacePane.cpp
index e52fb69200..71a044d658 100644
--- a/Source/Core/DolphinQt/Settings/InterfacePane.cpp
+++ b/Source/Core/DolphinQt/Settings/InterfacePane.cpp
@@ -22,6 +22,8 @@
#include "Core/AchievementManager.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/UISettings.h"
+#include "Core/Core.h"
+#include "Core/System.h"
#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/ConfigControls/ConfigChoice.h"
@@ -95,6 +97,10 @@ InterfacePane::InterfacePane(QWidget* parent) : QWidget(parent)
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&InterfacePane::UpdateShowDebuggingCheckbox);
+ connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
+ &InterfacePane::OnEmulationStateChanged);
+
+ OnEmulationStateChanged(Core::GetState(Core::System::GetInstance()));
}
void InterfacePane::CreateLayout()
@@ -168,12 +174,15 @@ void InterfacePane::CreateUI()
new ConfigBool(tr("Hotkeys Require Window Focus"), Config::MAIN_FOCUSED_HOTKEYS);
m_checkbox_disable_screensaver =
new ConfigBool(tr("Inhibit Screensaver During Emulation"), Config::MAIN_DISABLE_SCREENSAVER);
+ m_checkbox_time_tracking =
+ new ConfigBool(tr("Enable Play Time Tracking"), Config::MAIN_TIME_TRACKING);
groupbox_layout->addWidget(m_checkbox_use_builtin_title_database);
groupbox_layout->addWidget(m_checkbox_use_covers);
groupbox_layout->addWidget(m_checkbox_show_debugging_ui);
groupbox_layout->addWidget(m_checkbox_focused_hotkeys);
groupbox_layout->addWidget(m_checkbox_disable_screensaver);
+ groupbox_layout->addWidget(m_checkbox_time_tracking);
}
void InterfacePane::CreateInGame()
@@ -313,6 +322,12 @@ void InterfacePane::OnLanguageChanged()
tr("You must restart Dolphin in order for the change to take effect."));
}
+void InterfacePane::OnEmulationStateChanged(Core::State state)
+{
+ const bool uninitialized = state == Core::State::Uninitialized;
+ m_checkbox_time_tracking->setEnabled(uninitialized);
+}
+
void InterfacePane::AddDescriptions()
{
static constexpr char TR_TITLE_DATABASE_DESCRIPTION[] = QT_TR_NOOP(
@@ -341,6 +356,10 @@ void InterfacePane::AddDescriptions()
static constexpr char TR_DISABLE_SCREENSAVER_DESCRIPTION[] =
QT_TR_NOOP("Disables your screensaver while running a game."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
+ static constexpr char TR_TIME_TRACKING[] = QT_TR_NOOP(
+ "Tracks the time you spend playing games and shows it in the List View (as hours/minutes)."
+ "<br><br>This setting cannot be changed while emulation is active."
+ "<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_CONFIRM_ON_STOP_DESCRIPTION[] =
QT_TR_NOOP("Prompts you to confirm that you want to end emulation when you press Stop."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
@@ -394,6 +413,8 @@ void InterfacePane::AddDescriptions()
m_checkbox_disable_screensaver->SetDescription(tr(TR_DISABLE_SCREENSAVER_DESCRIPTION));
+ m_checkbox_time_tracking->SetDescription(tr(TR_TIME_TRACKING));
+
m_checkbox_confirm_on_stop->SetDescription(tr(TR_CONFIRM_ON_STOP_DESCRIPTION));
m_checkbox_use_panic_handlers->SetDescription(tr(TR_USE_PANIC_HANDLERS_DESCRIPTION));
diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.h b/Source/Core/DolphinQt/Settings/InterfacePane.h
index 90a81d2114..ffe1efa087 100644
--- a/Source/Core/DolphinQt/Settings/InterfacePane.h
+++ b/Source/Core/DolphinQt/Settings/InterfacePane.h
@@ -13,6 +13,11 @@ class QVBoxLayout;
class ToolTipCheckBox;
class ToolTipComboBox;
+namespace Core
+{
+enum class State;
+}
+
class InterfacePane final : public QWidget
{
Q_OBJECT
@@ -30,6 +35,8 @@ private:
void OnUserStyleChanged();
void OnLanguageChanged();
+ void OnEmulationStateChanged(Core::State state);
+
QVBoxLayout* m_main_layout;
ConfigStringChoice* m_combobox_language;
@@ -42,6 +49,7 @@ private:
ConfigBool* m_checkbox_focused_hotkeys;
ConfigBool* m_checkbox_use_covers;
ConfigBool* m_checkbox_disable_screensaver;
+ ConfigBool* m_checkbox_time_tracking;
ConfigBool* m_checkbox_confirm_on_stop;
ConfigBool* m_checkbox_use_panic_handlers;