diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 4704b340b2..1e4e6f403c 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -541,6 +541,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..663df65d8e 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -459,6 +459,8 @@ const Info MAIN_GAMELIST_COLUMN_BLOCK_SIZE{{System::Main, "GameList", "Col false}; const Info MAIN_GAMELIST_COLUMN_COMPRESSION{{System::Main, "GameList", "ColumnCompression"}, false}; +const Info MAIN_GAMELIST_COLUMN_TIME_PLAYED{{System::Main, "GameList", "ColumnTimePlayed"}, + true}; const Info 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..2dc4d29987 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -295,6 +295,7 @@ extern const Info MAIN_GAMELIST_COLUMN_FILE_SIZE; extern const Info MAIN_GAMELIST_COLUMN_FILE_FORMAT; extern const Info MAIN_GAMELIST_COLUMN_BLOCK_SIZE; extern const Info MAIN_GAMELIST_COLUMN_COMPRESSION; +extern const Info MAIN_GAMELIST_COLUMN_TIME_PLAYED; extern const Info MAIN_GAMELIST_COLUMN_TAGS; // Main.FifoPlayer diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 21d722290e..495071cff4 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,42 @@ void SConfig::LoadSettings() Config::Load(); } +const std::string& SConfig::GetGameID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_game_id; +} + +const std::string& SConfig::GetGameTDBID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_gametdb_id; +} + +const std::string& SConfig::GetTitleName() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_name; +} + +const std::string& SConfig::GetTitleDescription() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_description; +} + +u64 SConfig::GetTitleID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_id; +} + +u16 SConfig::GetRevision() const +{ + std::lock_guard lock(m_metadata_lock); + return m_revision; +} + void SConfig::ResetRunningGameMetadata() { SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown); diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index 999cc9f86c..9dfc331232 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -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::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..785a172ca7 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -10,16 +10,20 @@ #include "AudioCommon/AudioCommon.h" #include "Common/CommonTypes.h" #include "Common/Event.h" +#include "Common/Timer.h" +#include "Core/ConfigManager.h" #include "Core/CPUThreadConfigCallback.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 { + CPUManager::CPUManager(Core::System& system) : m_system(system) { } @@ -63,6 +67,34 @@ void CPUManager::ExecutePendingJobs(std::unique_lock& 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 (m_state == State::Running) + { + auto diff_time = std::chrono::duration_cast(curr_time - prev_time); + time_played.AddTime(diff_time); + } + + 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 +103,9 @@ 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 + auto timing = std::thread(&CPUManager::StartTimePlayedTimer, this); + std::unique_lock state_lock(m_state_change_lock); while (m_state != State::PowerDown) { @@ -165,6 +200,11 @@ void CPUManager::Run() break; } } + + // m_timer_finish.notify_one(); + m_time_played_finish_sync.Set(); + timing.join(); + state_lock.unlock(); Host_UpdateDisasmDialog(); } diff --git a/Source/Core/Core/HW/CPU.h b/Source/Core/Core/HW/CPU.h index 57e8a31a29..9550e016df 100644 --- a/Source/Core/Core/HW/CPU.h +++ b/Source/Core/Core/HW/CPU.h @@ -3,12 +3,14 @@ #pragma once +#include #include #include #include #include namespace Common + { class Event; } @@ -102,6 +104,7 @@ public: private: void FlushStepSyncEventLocked(); void ExecutePendingJobs(std::unique_lock& 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> 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..1a006fdce6 --- /dev/null +++ b/Source/Core/Core/TimePlayed.cpp @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include "Common/FileUtil.h" +#include "Common/IniFile.h" +#include "TimePlayed.h" + +// used for QT interface - general access to time played for games +TimePlayed::TimePlayed() +{ + m_game_id = "None"; + ini_path = File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini"; + ini.Load(ini_path); + time_list = ini.GetOrCreateSection("Time Played"); +} + +void FilterUnsafeCharacters(std::string& game_id) +{ + const std::string forbiddenChars = "\\/:?\"<>|"; + for (auto& chr : game_id) + { + if (forbiddenChars.find(chr) != std::string::npos) + { + chr = '_'; + } + } +} + +TimePlayed::TimePlayed(std::string game_id) +{ + // filter for unsafe characters + FilterUnsafeCharacters(game_id); + + m_game_id = game_id; + ini_path = File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini"; + ini.Load(ini_path); + time_list = ini.GetOrCreateSection("Time Played"); +} + +void TimePlayed::AddTime(std::chrono::milliseconds time_emulated) +{ + if (m_game_id == "None") + { + return; + } + + u64 previous_time; + time_list->Get(m_game_id, &previous_time); + time_list->Set(m_game_id, previous_time + u64(time_emulated.count())); + ini.Save(ini_path); +} + +u64 TimePlayed::GetTimePlayed() +{ + if (m_game_id == "None") + { + return 0; + } + + u64 previous_time; + time_list->Get(m_game_id, &previous_time); + return previous_time; +} + +u64 TimePlayed::GetTimePlayed(std::string game_id) +{ + FilterUnsafeCharacters(game_id); + u64 previous_time; + time_list->Get(game_id, &previous_time); + return previous_time; +} diff --git a/Source/Core/Core/TimePlayed.h b/Source/Core/Core/TimePlayed.h new file mode 100644 index 0000000000..71f631cd4f --- /dev/null +++ b/Source/Core/Core/TimePlayed.h @@ -0,0 +1,22 @@ +#pragma once +#include "Common/CommonTypes.h" +#include "Common/IniFile.h" + +class TimePlayed +{ +public: + TimePlayed(); + TimePlayed(std::string game_id); + + void AddTime(std::chrono::milliseconds time_emulated); + + u64 GetTimePlayed(); + u64 GetTimePlayed(std::string game_id); + +private: + std::string m_game_id; + Common::IniFile ini; + std::string ini_path; + + Common::IniFile::Section* 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 @@ + @@ -1125,6 +1126,7 @@ + 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..28ab518425 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -9,6 +9,7 @@ #include #include "Core/Config/MainSettings.h" +#include "Core/TimePlayed.h" #include "DiscIO/Enums.h" @@ -57,6 +58,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return QVariant(); const UICommon::GameFile& game = *m_games[index.row()]; + TimePlayed timer; switch (static_cast(index.column())) { @@ -187,6 +189,22 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return compression.isEmpty() ? tr("No Compression") : compression; } break; + case Column::TimePlayed: + if (role == Qt::DisplayRole || role == SORT_ROLE) + { + std::string game_id = game.GetGameID(); + std::chrono::milliseconds total_time(timer.GetTimePlayed(game_id)); + std::chrono::minutes total_minutes = + std::chrono::duration_cast(total_time); + std::chrono::hours total_hours = 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() - total_hours.count() * 60); + return formatted_time; + } + break; case Column::Tags: if (role == Qt::DisplayRole || role == SORT_ROLE) { @@ -232,6 +250,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: diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index b76e0a37c2..0ca28c1c4a 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -58,6 +58,7 @@ public: FileFormat, BlockSize, Compression, + TimePlayed, Tags, Count, }; 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);