diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index b17a143d3..880393be9 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -172,6 +172,8 @@ add_executable(citra-qt multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h + play_time_manager.cpp + play_time_manager.h precompiled_headers.h uisettings.cpp uisettings.h diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index ab2a65752..13aa7f9c1 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -790,6 +790,7 @@ void Config::ReadUIGameListValues() { ReadBasicSetting(UISettings::values.show_region_column); ReadBasicSetting(UISettings::values.show_type_column); ReadBasicSetting(UISettings::values.show_size_column); + ReadBasicSetting(UISettings::values.show_play_time_column); const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites")); for (int i = 0; i < favorites_size; i++) { @@ -1272,6 +1273,7 @@ void Config::SaveUIGameListValues() { WriteBasicSetting(UISettings::values.show_region_column); WriteBasicSetting(UISettings::values.show_type_column); WriteBasicSetting(UISettings::values.show_size_column); + WriteBasicSetting(UISettings::values.show_play_time_column); qt_config->beginWriteArray(QStringLiteral("favorites")); for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) { diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 4e502dadf..59f5d1457 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -306,7 +306,8 @@ void GameList::OnFilterCloseClicked() { main_window->filterBarSetChecked(false); } -GameList::GameList(GMainWindow* parent) : QWidget{parent} { +GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent) + : QWidget{parent}, play_time_manager{play_time_manager_} { watcher = new QFileSystemWatcher(this); connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory, Qt::UniqueConnection); @@ -522,7 +523,8 @@ void GameList::PopupHeaderContextMenu(const QPoint& menu_location) { {tr("Compatibility"), &UISettings::values.show_compat_column}, {tr("Region"), &UISettings::values.show_region_column}, {tr("File type"), &UISettings::values.show_type_column}, - {tr("Size"), &UISettings::values.show_size_column}}; + {tr("Size"), &UISettings::values.show_size_column}, + {tr("Play time"), &UISettings::values.show_play_time_column}}; QActionGroup* column_group = new QActionGroup(this); column_group->setExclusive(false); @@ -544,6 +546,7 @@ void GameList::UpdateColumnVisibility() { tree_view->setColumnHidden(COLUMN_REGION, !UISettings::values.show_region_column); tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_type_column); tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size_column); + tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time_column); } #ifdef ENABLE_OPENGL @@ -591,6 +594,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QAction* uninstall_update = uninstall_menu->addAction(tr("Update")); QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); + QAction* remove_play_time_data = context_menu.addAction(tr("Remove Play Time Data")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); #if !defined(__APPLE__) @@ -712,6 +716,8 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr }); connect(dump_romfs, &QAction::triggered, this, [this, path, program_id] { emit DumpRomFSRequested(path, program_id); }); + connect(remove_play_time_data, &QAction::triggered, + [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); @@ -933,6 +939,7 @@ void GameList::RetranslateUI() { item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, tr("Region")); item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); + item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time")); } void GameListSearchField::changeEvent(QEvent* event) { @@ -964,7 +971,7 @@ void GameList::PopulateAsync(QVector& game_dirs) { emit ShouldCancelWorker(); - GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list); + GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list, play_time_manager); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index 99f6b05d2..61670c201 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -9,6 +9,7 @@ #include #include #include "citra_qt/compatibility_list.h" +#include "citra_qt/play_time_manager.h" #include "common/common_types.h" #include "uisettings.h" @@ -60,10 +61,11 @@ public: COLUMN_REGION, COLUMN_FILE_TYPE, COLUMN_SIZE, + COLUMN_PLAY_TIME, COLUMN_COUNT, // Number of columns }; - explicit GameList(GMainWindow* parent = nullptr); + explicit GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent = nullptr); ~GameList() override; QString GetLastFilterResultItem() const; @@ -97,6 +99,7 @@ signals: void OpenFolderRequested(u64 program_id, GameListOpenTarget target); void CreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target); + void RemovePlayTimeRequested(u64 program_id); void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const QString file); @@ -142,6 +145,8 @@ private: CompatibilityList compatibility_list; friend class GameListSearchField; + + const PlayTime::PlayTimeManager& play_time_manager; }; Q_DECLARE_METATYPE(GameListOpenTarget); diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 16a7b5a8f..c41e43df4 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -18,6 +18,7 @@ #include #include #include +#include "citra_qt/play_time_manager.h" #include "citra_qt/uisettings.h" #include "citra_qt/util/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 { public: static constexpr int GameDirRole = Qt::UserRole + 2; diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index e4471b6ae..6755d121d 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -27,8 +27,10 @@ bool HasSupportedFileExtension(const std::string& file_name) { } // Anonymous namespace GameListWorker::GameListWorker(QVector& game_dirs, - const CompatibilityList& compatibility_list) - : game_dirs(game_dirs), compatibility_list(compatibility_list) {} + const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager_) + : game_dirs(game_dirs), + compatibility_list(compatibility_list), play_time_manager{play_time_manager_} {} GameListWorker::~GameListWorker() = default; @@ -112,6 +114,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(FileUtil::GetSize(physical_name)), + new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), }, parent_dir); diff --git a/src/citra_qt/game_list_worker.h b/src/citra_qt/game_list_worker.h index 60012e62a..09da74f4f 100644 --- a/src/citra_qt/game_list_worker.h +++ b/src/citra_qt/game_list_worker.h @@ -13,6 +13,7 @@ #include #include #include "citra_qt/compatibility_list.h" +#include "citra_qt/play_time_manager.h" #include "common/common_types.h" namespace Service::FS { @@ -30,7 +31,8 @@ class GameListWorker : public QObject, public QRunnable { public: GameListWorker(QVector& game_dirs, - const CompatibilityList& compatibility_list); + const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager_); ~GameListWorker() override; /// Starts the processing of directory tree information. @@ -60,6 +62,7 @@ private: QVector& game_dirs; const CompatibilityList& compatibility_list; + const PlayTime::PlayTimeManager& play_time_manager; QStringList watch_list; std::atomic_bool stop_processing; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 6d9d9c4b7..c23360711 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -61,6 +61,7 @@ #include "citra_qt/movie/movie_play_dialog.h" #include "citra_qt/movie/movie_record_dialog.h" #include "citra_qt/multiplayer/state.h" +#include "citra_qt/play_time_manager.h" #include "citra_qt/qt_image_interface.h" #include "citra_qt/uisettings.h" #include "citra_qt/updater/updater.h" @@ -210,6 +211,8 @@ GMainWindow::GMainWindow(Core::System& system_) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + play_time_manager = std::make_unique(); + Network::Init(); movie.SetPlaybackCompletionCallback([this] { @@ -364,7 +367,7 @@ void GMainWindow::InitializeWidgets() { secondary_window->hide(); secondary_window->setParent(nullptr); - game_list = new GameList(this); + game_list = new GameList(*play_time_manager, this); ui->horizontalLayout->addWidget(game_list); game_list_placeholder = new GameListPlaceholder(this); @@ -843,6 +846,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::RemovePlayTimeRequested, this, + &GMainWindow::OnGameListRemovePlayTimeData); connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, &GMainWindow::OnGameListNavigateToGamedbEntry); connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); @@ -1238,7 +1243,11 @@ bool GMainWindow::LoadROM(const QString& filename) { game_title = QString::fromStdString(title); UpdateWindowTitle(); + u64 title_id; + system.GetAppLoader().ReadProgramId(title_id); + game_path = filename; + game_title_id = title_id; system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt"); return true; @@ -1460,6 +1469,7 @@ void GMainWindow::ShutdownGame() { UpdateWindowTitle(); game_path.clear(); + game_title_id = 0; // Update the GUI UpdateMenuState(); @@ -1647,6 +1657,17 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); } +void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { + if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) != QMessageBox::Yes) { + return; + } + + play_time_manager->ResetProgramPlayTime(program_id); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list) { auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); @@ -2179,6 +2200,9 @@ void GMainWindow::OnStartGame() { UpdateMenuState(); + play_time_manager->SetProgramId(game_title_id); + play_time_manager->Start(); + discord_rpc->Update(); #ifdef __unix__ @@ -2201,6 +2225,8 @@ void GMainWindow::OnPauseGame() { emu_thread->SetRunning(false); qt_cameras->PauseCameras(); + play_time_manager->Stop(); + UpdateMenuState(); AllowOSSleep(); @@ -2220,6 +2246,10 @@ void GMainWindow::OnPauseContinueGame() { } void GMainWindow::OnStopGame() { + play_time_manager->Stop(); + // Update game list to show new play time + game_list->PopulateAsync(UISettings::values.game_dirs); + ShutdownGame(); graphics_api_button->setEnabled(true); Settings::RestoreGlobalState(false); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 744e23674..55ee98381 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -64,6 +64,10 @@ namespace DiscordRPC { class DiscordInterface; } +namespace PlayTime { +class PlayTimeManager; +} + namespace Core { class Movie; } @@ -94,6 +98,7 @@ public: ~GMainWindow(); GameList* game_list; + std::unique_ptr play_time_manager; std::unique_ptr discord_rpc; bool DropAction(QDropEvent* event); @@ -225,6 +230,7 @@ private slots: /// Called whenever a user selects a game in the game list widget. void OnGameListLoadFile(QString game_path); void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); + void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, @@ -299,6 +305,7 @@ private: void UpdateWindowTitle(); void UpdateUISettings(); void RetranslateStatusBar(); + void RemovePlayTimeData(u64 program_id); void InstallCIA(QStringList filepaths); void HideMouseCursor(); void ShowMouseCursor(); @@ -343,6 +350,8 @@ private: QString game_title; // The path to the game currently running QString game_path; + // The title id of the game currently running + u64 game_title_id; bool auto_paused = false; bool auto_muted = false; diff --git a/src/citra_qt/play_time_manager.cpp b/src/citra_qt/play_time_manager.cpp new file mode 100644 index 000000000..03c2e1b16 --- /dev/null +++ b/src/citra_qt/play_time_manager.cpp @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2024 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "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 elements(num_elements); + + if (file.ReadSpan(elements) != num_elements) { + return false; + } + + for (const auto& [program_id, play_time] : elements) { + if (program_id != 0) { + out_play_time_db[program_id] = play_time; + } + } + } + + return true; +} + +[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); + + FileUtil::IOFile file{filename, "wb"}; + if (!file.IsOpen()) { + LOG_ERROR(Frontend, "Failed to open play time file: {}", filename); + return false; + } + + std::vector elements; + elements.reserve(play_time_db.size()); + + for (auto& [program_id, play_time] : play_time_db) { + if (program_id != 0) { + elements.push_back(PlayTimeElement{program_id, play_time}); + } + } + + return file.WriteSpan(elements) == elements.size(); +} + +} // namespace + +PlayTimeManager::PlayTimeManager() { + if (!ReadPlayTimeFile(database)) { + LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default."); + } +} + +PlayTimeManager::~PlayTimeManager() { + Save(); +} + +void PlayTimeManager::SetProgramId(u64 program_id) { + running_program_id = program_id; +} + +void PlayTimeManager::Start() { + play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); }); +} + +void PlayTimeManager::Stop() { + play_time_thread = {}; +} + +void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) { + Common::SetCurrentThreadName("PlayTimeReport"); + + using namespace std::literals::chrono_literals; + using std::chrono::seconds; + using std::chrono::steady_clock; + + auto timestamp = steady_clock::now(); + + const auto GetDuration = [&]() -> u64 { + const auto last_timestamp = std::exchange(timestamp, steady_clock::now()); + const auto duration = std::chrono::duration_cast(timestamp - last_timestamp); + return static_cast(duration.count()); + }; + + while (!stop_token.stop_requested()) { + Common::StoppableTimedWait(stop_token, 30s); + + database[running_program_id] += GetDuration(); + Save(); + } +} + +void PlayTimeManager::Save() { + if (!WritePlayTimeFile(database)) { + LOG_ERROR(Frontend, "Failed to update play time database!"); + } +} + +u64 PlayTimeManager::GetPlayTime(u64 program_id) const { + auto it = database.find(program_id); + if (it != database.end()) { + return it->second; + } else { + return 0; + } +} + +void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { + database.erase(program_id); + Save(); +} + +QString ReadablePlayTime(qulonglong time_seconds) { + if (time_seconds == 0) { + return {}; + } + const auto time_minutes = std::max(static_cast(time_seconds) / 60, 1.0); + const auto time_hours = static_cast(time_seconds) / 3600; + const bool is_minutes = time_minutes < 60; + const char* unit = is_minutes ? "m" : "h"; + const auto value = is_minutes ? time_minutes : time_hours; + + return QStringLiteral("%L1 %2") + .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) + .arg(QString::fromUtf8(unit)); +} + +} // namespace PlayTime diff --git a/src/citra_qt/play_time_manager.h b/src/citra_qt/play_time_manager.h new file mode 100644 index 000000000..c8ba48db7 --- /dev/null +++ b/src/citra_qt/play_time_manager.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/polyfill_thread.h" + +namespace PlayTime { + +using ProgramId = u64; +using PlayTime = u64; +using PlayTimeDatabase = std::map; + +class PlayTimeManager { +public: + explicit PlayTimeManager(); + ~PlayTimeManager(); + + PlayTimeManager(const PlayTimeManager&) = delete; + PlayTimeManager& operator=(const PlayTimeManager&) = delete; + + u64 GetPlayTime(u64 program_id) const; + void ResetProgramPlayTime(u64 program_id); + void SetProgramId(u64 program_id); + void Start(); + void Stop(); + +private: + void AutoTimestamp(std::stop_token stop_token); + void Save(); + + PlayTimeDatabase database; + u64 running_program_id; + std::jthread play_time_thread; +}; + +QString ReadablePlayTime(qulonglong time_seconds); + +} // namespace PlayTime diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index e85e139a9..dca889ec7 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -103,6 +103,7 @@ struct Values { Settings::Setting show_region_column{true, "show_region_column"}; Settings::Setting show_type_column{true, "show_type_column"}; Settings::Setting show_size_column{true, "show_size_column"}; + Settings::Setting show_play_time_column{true, "show_play_time_column"}; Settings::Setting screenshot_resolution_factor{0, "screenshot_resolution_factor"}; Settings::SwitchableSetting screenshot_path{"", "screenshotPath"}; diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 3d3840681..7b6480d71 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -54,6 +54,7 @@ #define SHADER_DIR "shaders" #define STATES_DIR "states" #define ICONS_DIR "icons" +#define PLAY_TIME_DIR "play_time" // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 1c6bbcf3a..73abd87a6 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -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::StatesDir, user_path + STATES_DIR DIR_SEP); g_paths.emplace(UserPath::IconsDir, user_path + ICONS_DIR DIR_SEP); + g_paths.emplace(UserPath::PlayTimeDir, user_path + PLAY_TIME_DIR DIR_SEP); g_default_paths = g_paths; } diff --git a/src/common/file_util.h b/src/common/file_util.h index 379256bfb..418a7cedd 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -41,6 +41,7 @@ enum class UserPath { SysDataDir, UserDir, IconsDir, + PlayTimeDir, }; // Replaces install-specific paths with standard placeholders, and back again @@ -347,6 +348,59 @@ public: return WriteArray(str.data(), str.length()); } + /** + * Reads a span of T data from a file sequentially. + * This function reads from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully read. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks read permissions + * - Attempting to read beyond the end-of-file + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully read. + */ + template + [[nodiscard]] size_t ReadSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fread(data.data(), sizeof(T), data.size(), m_file); + } + + /** + * Writes a span of T data to a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully written. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks write permissions + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully written. + */ + template + [[nodiscard]] size_t WriteSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fwrite(data.data(), sizeof(T), data.size(), m_file); + } + [[nodiscard]] bool IsOpen() const { return nullptr != m_file; } diff --git a/src/common/polyfill_thread.h b/src/common/polyfill_thread.h index 3146075f3..bf8cb4ecb 100644 --- a/src/common/polyfill_thread.h +++ b/src/common/polyfill_thread.h @@ -12,8 +12,11 @@ #ifdef __cpp_lib_jthread +#include +#include #include #include +#include namespace Common { @@ -22,11 +25,23 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) { cv.wait(lock, token, std::move(pred)); } +template +bool StoppableTimedWait(std::stop_token token, const std::chrono::duration& rel_time) { + std::condition_variable_any cv; + std::mutex m; + + // Perform the timed wait. + std::unique_lock lk{m}; + return !cv.wait_for(lk, token, rel_time, [&] { return token.stop_requested(); }); +} + } // namespace Common #else #include +#include +#include #include #include #include @@ -333,6 +348,30 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) { cv.wait(lock, [&] { return pred() || token.stop_requested(); }); } +template +bool StoppableTimedWait(std::stop_token token, const std::chrono::duration& rel_time) { + if (token.stop_requested()) { + return false; + } + + bool stop_requested = false; + std::condition_variable cv; + std::mutex m; + + std::stop_callback cb(token, [&] { + // Wake up the waiting thread. + { + std::scoped_lock lk{m}; + stop_requested = true; + } + cv.notify_one(); + }); + + // Perform the timed wait. + std::unique_lock lk{m}; + return !cv.wait_for(lk, rel_time, [&] { return stop_requested; }); +} + } // namespace Common #endif // __cpp_lib_jthread