citra_qt: Track play time

Co-Authored-By: Mario Davó <66087392+mdmrk@users.noreply.github.com>
This commit is contained in:
FearlessTobi 2024-02-08 00:17:16 +01:00
parent 4abe3f3436
commit a4b16bfb1f
16 changed files with 394 additions and 8 deletions

View file

@ -172,6 +172,8 @@ add_executable(citra-qt
multiplayer/state.cpp multiplayer/state.cpp
multiplayer/state.h multiplayer/state.h
multiplayer/validation.h multiplayer/validation.h
play_time_manager.cpp
play_time_manager.h
precompiled_headers.h precompiled_headers.h
uisettings.cpp uisettings.cpp
uisettings.h uisettings.h

View file

@ -790,6 +790,7 @@ void Config::ReadUIGameListValues() {
ReadBasicSetting(UISettings::values.show_region_column); ReadBasicSetting(UISettings::values.show_region_column);
ReadBasicSetting(UISettings::values.show_type_column); ReadBasicSetting(UISettings::values.show_type_column);
ReadBasicSetting(UISettings::values.show_size_column); ReadBasicSetting(UISettings::values.show_size_column);
ReadBasicSetting(UISettings::values.show_play_time_column);
const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites")); const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
for (int i = 0; i < favorites_size; i++) { for (int i = 0; i < favorites_size; i++) {
@ -1272,6 +1273,7 @@ void Config::SaveUIGameListValues() {
WriteBasicSetting(UISettings::values.show_region_column); WriteBasicSetting(UISettings::values.show_region_column);
WriteBasicSetting(UISettings::values.show_type_column); WriteBasicSetting(UISettings::values.show_type_column);
WriteBasicSetting(UISettings::values.show_size_column); WriteBasicSetting(UISettings::values.show_size_column);
WriteBasicSetting(UISettings::values.show_play_time_column);
qt_config->beginWriteArray(QStringLiteral("favorites")); qt_config->beginWriteArray(QStringLiteral("favorites"));
for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) { for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {

View file

@ -306,7 +306,8 @@ void GameList::OnFilterCloseClicked() {
main_window->filterBarSetChecked(false); 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); watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory, connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory,
Qt::UniqueConnection); Qt::UniqueConnection);
@ -522,7 +523,8 @@ void GameList::PopupHeaderContextMenu(const QPoint& menu_location) {
{tr("Compatibility"), &UISettings::values.show_compat_column}, {tr("Compatibility"), &UISettings::values.show_compat_column},
{tr("Region"), &UISettings::values.show_region_column}, {tr("Region"), &UISettings::values.show_region_column},
{tr("File type"), &UISettings::values.show_type_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); QActionGroup* column_group = new QActionGroup(this);
column_group->setExclusive(false); 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_REGION, !UISettings::values.show_region_column);
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_type_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_SIZE, !UISettings::values.show_size_column);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time_column);
} }
#ifdef ENABLE_OPENGL #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_update = uninstall_menu->addAction(tr("Update"));
QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); 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")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
#if !defined(__APPLE__) #if !defined(__APPLE__)
@ -712,6 +716,8 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
}); });
connect(dump_romfs, &QAction::triggered, this, connect(dump_romfs, &QAction::triggered, this,
[this, path, program_id] { emit DumpRomFSRequested(path, program_id); }); [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]() { connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list); 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_REGION, Qt::Horizontal, tr("Region"));
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); 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) { void GameListSearchField::changeEvent(QEvent* event) {
@ -964,7 +971,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
emit ShouldCancelWorker(); 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::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,

View file

@ -9,6 +9,7 @@
#include <QVector> #include <QVector>
#include <QWidget> #include <QWidget>
#include "citra_qt/compatibility_list.h" #include "citra_qt/compatibility_list.h"
#include "citra_qt/play_time_manager.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "uisettings.h" #include "uisettings.h"
@ -60,10 +61,11 @@ public:
COLUMN_REGION, COLUMN_REGION,
COLUMN_FILE_TYPE, COLUMN_FILE_TYPE,
COLUMN_SIZE, COLUMN_SIZE,
COLUMN_PLAY_TIME,
COLUMN_COUNT, // Number of columns COLUMN_COUNT, // Number of columns
}; };
explicit GameList(GMainWindow* parent = nullptr); explicit GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent = nullptr);
~GameList() override; ~GameList() override;
QString GetLastFilterResultItem() const; QString GetLastFilterResultItem() const;
@ -97,6 +99,7 @@ signals:
void OpenFolderRequested(u64 program_id, GameListOpenTarget target); void OpenFolderRequested(u64 program_id, GameListOpenTarget target);
void CreateShortcut(u64 program_id, const std::string& game_path, void CreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target); GameListShortcutTarget target);
void RemovePlayTimeRequested(u64 program_id);
void NavigateToGamedbEntryRequested(u64 program_id, void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const QString file); void OpenPerGameGeneralRequested(const QString file);
@ -142,6 +145,8 @@ private:
CompatibilityList compatibility_list; CompatibilityList compatibility_list;
friend class GameListSearchField; friend class GameListSearchField;
const PlayTime::PlayTimeManager& play_time_manager;
}; };
Q_DECLARE_METATYPE(GameListOpenTarget); Q_DECLARE_METATYPE(GameListOpenTarget);

View file

@ -18,6 +18,7 @@
#include <QStandardItem> #include <QStandardItem>
#include <QString> #include <QString>
#include <QWidget> #include <QWidget>
#include "citra_qt/play_time_manager.h"
#include "citra_qt/uisettings.h" #include "citra_qt/uisettings.h"
#include "citra_qt/util/util.h" #include "citra_qt/util/util.h"
#include "common/file_util.h" #include "common/file_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 { class GameListDir : public GameListItem {
public: public:
static constexpr int GameDirRole = Qt::UserRole + 2; static constexpr int GameDirRole = Qt::UserRole + 2;

View file

@ -27,8 +27,10 @@ bool HasSupportedFileExtension(const std::string& file_name) {
} // Anonymous namespace } // Anonymous namespace
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs, GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
const CompatibilityList& compatibility_list) const CompatibilityList& compatibility_list,
: game_dirs(game_dirs), compatibility_list(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; GameListWorker::~GameListWorker() = default;
@ -112,6 +114,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
new GameListItem( new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)), new GameListItemSize(FileUtil::GetSize(physical_name)),
new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
}, },
parent_dir); parent_dir);

View file

@ -13,6 +13,7 @@
#include <QString> #include <QString>
#include <QVector> #include <QVector>
#include "citra_qt/compatibility_list.h" #include "citra_qt/compatibility_list.h"
#include "citra_qt/play_time_manager.h"
#include "common/common_types.h" #include "common/common_types.h"
namespace Service::FS { namespace Service::FS {
@ -30,7 +31,8 @@ class GameListWorker : public QObject, public QRunnable {
public: public:
GameListWorker(QVector<UISettings::GameDir>& game_dirs, GameListWorker(QVector<UISettings::GameDir>& game_dirs,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list,
const PlayTime::PlayTimeManager& play_time_manager_);
~GameListWorker() override; ~GameListWorker() override;
/// Starts the processing of directory tree information. /// Starts the processing of directory tree information.
@ -60,6 +62,7 @@ private:
QVector<UISettings::GameDir>& game_dirs; QVector<UISettings::GameDir>& game_dirs;
const CompatibilityList& compatibility_list; const CompatibilityList& compatibility_list;
const PlayTime::PlayTimeManager& play_time_manager;
QStringList watch_list; QStringList watch_list;
std::atomic_bool stop_processing; std::atomic_bool stop_processing;

View file

@ -61,6 +61,7 @@
#include "citra_qt/movie/movie_play_dialog.h" #include "citra_qt/movie/movie_play_dialog.h"
#include "citra_qt/movie/movie_record_dialog.h" #include "citra_qt/movie/movie_record_dialog.h"
#include "citra_qt/multiplayer/state.h" #include "citra_qt/multiplayer/state.h"
#include "citra_qt/play_time_manager.h"
#include "citra_qt/qt_image_interface.h" #include "citra_qt/qt_image_interface.h"
#include "citra_qt/uisettings.h" #include "citra_qt/uisettings.h"
#include "citra_qt/updater/updater.h" #include "citra_qt/updater/updater.h"
@ -210,6 +211,8 @@ GMainWindow::GMainWindow(Core::System& system_)
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
discord_rpc->Update(); discord_rpc->Update();
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
Network::Init(); Network::Init();
movie.SetPlaybackCompletionCallback([this] { movie.SetPlaybackCompletionCallback([this] {
@ -364,7 +367,7 @@ void GMainWindow::InitializeWidgets() {
secondary_window->hide(); secondary_window->hide();
secondary_window->setParent(nullptr); secondary_window->setParent(nullptr);
game_list = new GameList(this); game_list = new GameList(*play_time_manager, this);
ui->horizontalLayout->addWidget(game_list); ui->horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this); game_list_placeholder = new GameListPlaceholder(this);
@ -843,6 +846,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
connect(game_list, &GameList::RemovePlayTimeRequested, this,
&GMainWindow::OnGameListRemovePlayTimeData);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry); &GMainWindow::OnGameListNavigateToGamedbEntry);
connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);
@ -1238,7 +1243,11 @@ bool GMainWindow::LoadROM(const QString& filename) {
game_title = QString::fromStdString(title); game_title = QString::fromStdString(title);
UpdateWindowTitle(); UpdateWindowTitle();
u64 title_id;
system.GetAppLoader().ReadProgramId(title_id);
game_path = filename; game_path = filename;
game_title_id = title_id;
system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt"); system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
return true; return true;
@ -1460,6 +1469,7 @@ void GMainWindow::ShutdownGame() {
UpdateWindowTitle(); UpdateWindowTitle();
game_path.clear(); game_path.clear();
game_title_id = 0;
// Update the GUI // Update the GUI
UpdateMenuState(); UpdateMenuState();
@ -1647,6 +1657,17 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) {
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); 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, void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list) { const CompatibilityList& compatibility_list) {
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
@ -2179,6 +2200,9 @@ void GMainWindow::OnStartGame() {
UpdateMenuState(); UpdateMenuState();
play_time_manager->SetProgramId(game_title_id);
play_time_manager->Start();
discord_rpc->Update(); discord_rpc->Update();
#ifdef __unix__ #ifdef __unix__
@ -2201,6 +2225,8 @@ void GMainWindow::OnPauseGame() {
emu_thread->SetRunning(false); emu_thread->SetRunning(false);
qt_cameras->PauseCameras(); qt_cameras->PauseCameras();
play_time_manager->Stop();
UpdateMenuState(); UpdateMenuState();
AllowOSSleep(); AllowOSSleep();
@ -2220,6 +2246,10 @@ void GMainWindow::OnPauseContinueGame() {
} }
void GMainWindow::OnStopGame() { void GMainWindow::OnStopGame() {
play_time_manager->Stop();
// Update game list to show new play time
game_list->PopulateAsync(UISettings::values.game_dirs);
ShutdownGame(); ShutdownGame();
graphics_api_button->setEnabled(true); graphics_api_button->setEnabled(true);
Settings::RestoreGlobalState(false); Settings::RestoreGlobalState(false);

View file

@ -64,6 +64,10 @@ namespace DiscordRPC {
class DiscordInterface; class DiscordInterface;
} }
namespace PlayTime {
class PlayTimeManager;
}
namespace Core { namespace Core {
class Movie; class Movie;
} }
@ -94,6 +98,7 @@ public:
~GMainWindow(); ~GMainWindow();
GameList* game_list; GameList* game_list;
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
bool DropAction(QDropEvent* event); bool DropAction(QDropEvent* event);
@ -225,6 +230,7 @@ private slots:
/// Called whenever a user selects a game in the game list widget. /// Called whenever a user selects a game in the game list widget.
void OnGameListLoadFile(QString game_path); void OnGameListLoadFile(QString game_path);
void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target);
void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id, void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list); const CompatibilityList& compatibility_list);
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
@ -299,6 +305,7 @@ private:
void UpdateWindowTitle(); void UpdateWindowTitle();
void UpdateUISettings(); void UpdateUISettings();
void RetranslateStatusBar(); void RetranslateStatusBar();
void RemovePlayTimeData(u64 program_id);
void InstallCIA(QStringList filepaths); void InstallCIA(QStringList filepaths);
void HideMouseCursor(); void HideMouseCursor();
void ShowMouseCursor(); void ShowMouseCursor();
@ -343,6 +350,8 @@ private:
QString game_title; QString game_title;
// The path to the game currently running // The path to the game currently running
QString game_path; QString game_path;
// The title id of the game currently running
u64 game_title_id;
bool auto_paused = false; bool auto_paused = false;
bool auto_muted = false; bool auto_muted = false;

View file

@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: 2024 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include "citra_qt/play_time_manager.h"
#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"
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<PlayTimeElement> elements(num_elements);
if (file.ReadSpan<PlayTimeElement>(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<PlayTimeElement> 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<PlayTimeElement>(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<seconds>(timestamp - last_timestamp);
return static_cast<u64>(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<double>(time_seconds) / 60, 1.0);
const auto time_hours = static_cast<double>(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

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2024 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QString>
#include <map>
#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<ProgramId, PlayTime>;
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

View file

@ -103,6 +103,7 @@ struct Values {
Settings::Setting<bool> show_region_column{true, "show_region_column"}; Settings::Setting<bool> show_region_column{true, "show_region_column"};
Settings::Setting<bool> show_type_column{true, "show_type_column"}; Settings::Setting<bool> show_type_column{true, "show_type_column"};
Settings::Setting<bool> show_size_column{true, "show_size_column"}; Settings::Setting<bool> show_size_column{true, "show_size_column"};
Settings::Setting<bool> show_play_time_column{true, "show_play_time_column"};
Settings::Setting<u16> screenshot_resolution_factor{0, "screenshot_resolution_factor"}; Settings::Setting<u16> screenshot_resolution_factor{0, "screenshot_resolution_factor"};
Settings::SwitchableSetting<std::string> screenshot_path{"", "screenshotPath"}; Settings::SwitchableSetting<std::string> screenshot_path{"", "screenshotPath"};

View file

@ -54,6 +54,7 @@
#define SHADER_DIR "shaders" #define SHADER_DIR "shaders"
#define STATES_DIR "states" #define STATES_DIR "states"
#define ICONS_DIR "icons" #define ICONS_DIR "icons"
#define PLAY_TIME_DIR "play_time"
// Filenames // Filenames
// Files in the directory returned by GetUserPath(UserPath::LogDir) // Files in the directory returned by GetUserPath(UserPath::LogDir)

View file

@ -818,6 +818,7 @@ void SetUserPath(const std::string& path) {
g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); 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::StatesDir, user_path + STATES_DIR DIR_SEP);
g_paths.emplace(UserPath::IconsDir, user_path + ICONS_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; g_default_paths = g_paths;
} }

View file

@ -41,6 +41,7 @@ enum class UserPath {
SysDataDir, SysDataDir,
UserDir, UserDir,
IconsDir, IconsDir,
PlayTimeDir,
}; };
// Replaces install-specific paths with standard placeholders, and back again // Replaces install-specific paths with standard placeholders, and back again
@ -347,6 +348,59 @@ public:
return WriteArray(str.data(), str.length()); 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 <typename T>
[[nodiscard]] size_t ReadSpan(std::span<T> data) const {
static_assert(std::is_trivially_copyable_v<T>, "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 <typename T>
[[nodiscard]] size_t WriteSpan(std::span<const T> data) const {
static_assert(std::is_trivially_copyable_v<T>, "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 { [[nodiscard]] bool IsOpen() const {
return nullptr != m_file; return nullptr != m_file;
} }

View file

@ -12,8 +12,11 @@
#ifdef __cpp_lib_jthread #ifdef __cpp_lib_jthread
#include <chrono>
#include <condition_variable>
#include <stop_token> #include <stop_token>
#include <thread> #include <thread>
#include <utility>
namespace Common { 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)); cv.wait(lock, token, std::move(pred));
} }
template <typename Rep, typename Period>
bool StoppableTimedWait(std::stop_token token, const std::chrono::duration<Rep, Period>& 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 } // namespace Common
#else #else
#include <atomic> #include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional> #include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
@ -333,6 +348,30 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) {
cv.wait(lock, [&] { return pred() || token.stop_requested(); }); cv.wait(lock, [&] { return pred() || token.stop_requested(); });
} }
template <typename Rep, typename Period>
bool StoppableTimedWait(std::stop_token token, const std::chrono::duration<Rep, Period>& 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 } // namespace Common
#endif // __cpp_lib_jthread #endif // __cpp_lib_jthread