Merge pull request #140 from sudachi-emu/feature-total-times-played

Added total times played support
This commit is contained in:
Jarrod Norwell 2024-05-27 12:17:31 +08:00 committed by GitHub
commit 39f1a62c8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 106 additions and 15 deletions

View File

@ -102,7 +102,7 @@ void PushInShowController(Core::System& system, AppletStorageChannel& channel) {
// ShowControllerFirmwareUpdateForSystem/ShowControllerKeyRemappingForSystem, // ShowControllerFirmwareUpdateForSystem/ShowControllerKeyRemappingForSystem,
// which sets this to the input param // which sets this to the input param
.style_set = Core::HID::NpadStyleSet::None, .style_set = Core::HID::NpadStyleSet::None,
.joy_hold_type = 0, .joy_hold_type = Frontend::NpadJoyDeviceHoldType::Vertical,
}; };
std::vector<u8> common_args_data(sizeof(common_args)); std::vector<u8> common_args_data(sizeof(common_args));
std::vector<u8> private_args_data(sizeof(private_args)); std::vector<u8> private_args_data(sizeof(private_args));

View File

@ -53,6 +53,8 @@ enum class ControllerSupportResult : u32 {
Cancel = 2, Cancel = 2,
}; };
enum class NpadJoyDeviceHoldType : u32 { Vertical = 0, Horizontal = 1 };
struct ControllerSupportArgPrivate { struct ControllerSupportArgPrivate {
u32 arg_private_size{}; u32 arg_private_size{};
u32 arg_size{}; u32 arg_size{};
@ -61,7 +63,7 @@ struct ControllerSupportArgPrivate {
ControllerSupportMode mode{}; ControllerSupportMode mode{};
ControllerSupportCaller caller{}; ControllerSupportCaller caller{};
Core::HID::NpadStyleSet style_set{}; Core::HID::NpadStyleSet style_set{};
u32 joy_hold_type{}; NpadJoyDeviceHoldType joy_hold_type{};
}; };
static_assert(sizeof(ControllerSupportArgPrivate) == 0x14, static_assert(sizeof(ControllerSupportArgPrivate) == 0x14,
"ControllerSupportArgPrivate has incorrect size."); "ControllerSupportArgPrivate has incorrect size.");

View File

@ -125,6 +125,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
connect(ui->show_play_time, &QCheckBox::stateChanged, this, connect(ui->show_play_time, &QCheckBox::stateChanged, this,
&ConfigureUi::RequestGameListUpdate); &ConfigureUi::RequestGameListUpdate);
connect(ui->show_total_times, &QCheckBox::stateChanged, this,
&ConfigureUi::RequestGameListUpdate);
connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ConfigureUi::RequestGameListUpdate); &ConfigureUi::RequestGameListUpdate);
connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged),
@ -170,6 +172,7 @@ void ConfigureUi::ApplyConfiguration() {
UISettings::values.show_size = ui->show_size->isChecked(); UISettings::values.show_size = ui->show_size->isChecked();
UISettings::values.show_types = ui->show_types->isChecked(); UISettings::values.show_types = ui->show_types->isChecked();
UISettings::values.show_play_time = ui->show_play_time->isChecked(); UISettings::values.show_play_time = ui->show_play_time->isChecked();
UISettings::values.show_total_times = ui->show_total_times->isChecked();
UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt(); UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt();
UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt(); UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt();
UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt();
@ -200,6 +203,7 @@ void ConfigureUi::SetConfiguration() {
ui->show_size->setChecked(UISettings::values.show_size.GetValue()); ui->show_size->setChecked(UISettings::values.show_size.GetValue());
ui->show_types->setChecked(UISettings::values.show_types.GetValue()); ui->show_types->setChecked(UISettings::values.show_types.GetValue());
ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue()); ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue());
ui->show_total_times->setChecked(UISettings::values.show_total_times.GetValue());
ui->game_icon_size_combobox->setCurrentIndex( ui->game_icon_size_combobox->setCurrentIndex(
ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue())); ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue()));
ui->folder_icon_size_combobox->setCurrentIndex( ui->folder_icon_size_combobox->setCurrentIndex(
@ -262,7 +266,7 @@ void ConfigureUi::InitializeLanguageComboBox() {
#else #else
const QString country = QLocale::countryToString(QLocale(locale).country()); const QString country = QLocale::countryToString(QLocale(locale).country());
ui->language_combobox->addItem(QStringLiteral("%1 (%2)").arg(lang, country), locale); ui->language_combobox->addItem(QStringLiteral("%1 (%2)").arg(lang, country), locale);
#endif #endif
} }
// Unlike other configuration changes, interface language changes need to be reflected on the // Unlike other configuration changes, interface language changes need to be reflected on the

View File

@ -111,6 +111,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="show_total_times">
<property name="text">
<string>Show Total Times Column</string>
</property>
</widget>
</item>
<item> <item>
<layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2"> <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2">
<item> <item>

View File

@ -344,6 +344,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
tree_view->setColumnHidden(COLUMN_TOTAL_TIMES, !UISettings::values.show_total_times);
item_model->setSortRole(GameListItemPath::SortRole); item_model->setSortRole(GameListItemPath::SortRole);
connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
@ -800,6 +801,7 @@ void GameList::RetranslateUI() {
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")); item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
item_model->setHeaderData(COLUMN_TOTAL_TIMES, Qt::Horizontal, tr("Total times"));
} }
void GameListSearchField::changeEvent(QEvent* event) { void GameListSearchField::changeEvent(QEvent* event) {
@ -828,6 +830,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
tree_view->setColumnHidden(COLUMN_TOTAL_TIMES, !UISettings::values.show_total_times);
// Cancel any existing worker. // Cancel any existing worker.
current_worker.reset(); current_worker.reset();

View File

@ -16,9 +16,9 @@
#include "common/common_types.h" #include "common/common_types.h"
#include "core/core.h" #include "core/core.h"
#include "uisettings.h"
#include "sudachi/compatibility_list.h" #include "sudachi/compatibility_list.h"
#include "sudachi/play_time_manager.h" #include "sudachi/play_time_manager.h"
#include "uisettings.h"
namespace Core { namespace Core {
class System; class System;
@ -77,6 +77,7 @@ public:
COLUMN_FILE_TYPE, COLUMN_FILE_TYPE,
COLUMN_SIZE, COLUMN_SIZE,
COLUMN_PLAY_TIME, COLUMN_PLAY_TIME,
COLUMN_TOTAL_TIMES,
COLUMN_COUNT, // Number of columns COLUMN_COUNT, // Number of columns
}; };

View File

@ -247,6 +247,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 GameListItemTotalTimes : public GameListItem {
public:
static constexpr int TotalTimesRole = SortRole;
GameListItemTotalTimes() = default;
explicit GameListItemTotalTimes(const qulonglong times_count) {
setData(times_count, TotalTimesRole);
}
void setData(const QVariant& value, int role) override {
qulonglong times_count = value.toULongLong();
GameListItem::setData(PlayTime::ReadableTotalTimes(times_count), Qt::DisplayRole);
GameListItem::setData(value, TotalTimesRole);
}
bool operator<(const QStandardItem& other) const override {
return data(TotalTimesRole).toULongLong() < other.data(TotalTimesRole).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

@ -215,7 +215,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
new GameListItem(file_type_string), new GameListItem(file_type_string),
new GameListItemSize(size), new GameListItemSize(size),
new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
}; new GameListItemTotalTimes(play_time_manager.GetTotalTimes(program_id))};
const auto patch_versions = GetGameListCachedObject( const auto patch_versions = GetGameListCachedObject(
fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] { fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {

View File

@ -18,6 +18,7 @@ void PlayTimeManager::SetProgramId(u64 program_id) {
inline void PlayTimeManager::UpdateTimestamp() { inline void PlayTimeManager::UpdateTimestamp() {
this->last_timestamp = std::chrono::steady_clock::now(); this->last_timestamp = std::chrono::steady_clock::now();
this->last_total_times = GetTotalTimes(this->running_program_id);
} }
void PlayTimeManager::Start() { void PlayTimeManager::Start() {
@ -50,12 +51,12 @@ void PlayTimeManager::Save() {
std::chrono::steady_clock::duration(now - this->last_timestamp)) std::chrono::steady_clock::duration(now - this->last_timestamp))
.count()); .count());
UpdateTimestamp(); UpdateTimestamp();
if (!UpdatePlayTime(running_program_id, duration)) { if (!UpdatePlayTime(running_program_id, duration, 1)) {
LOG_ERROR(Common, "Failed to update play time"); LOG_ERROR(Common, "Failed to update play time");
} }
} }
bool UpdatePlayTime(u64 program_id, u64 add_play_time) { bool UpdatePlayTime(u64 program_id, u64 add_play_time, u64 add_total_times) {
std::vector<PlayTimeElement> play_time_elements; std::vector<PlayTimeElement> play_time_elements;
if (!ReadPlayTimeFile(play_time_elements)) { if (!ReadPlayTimeFile(play_time_elements)) {
return false; return false;
@ -63,9 +64,11 @@ bool UpdatePlayTime(u64 program_id, u64 add_play_time) {
const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id); const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id);
if (it == play_time_elements.end()) { if (it == play_time_elements.end()) {
play_time_elements.push_back({.program_id = program_id, .play_time = add_play_time}); play_time_elements.push_back(
{.program_id = program_id, .play_time = add_play_time, .total_times = add_total_times});
} else { } else {
play_time_elements.at(it - play_time_elements.begin()).play_time += add_play_time; play_time_elements.at(it - play_time_elements.begin()).play_time += add_play_time;
play_time_elements.at(it - play_time_elements.begin()).total_times += add_total_times;
} }
if (!WritePlayTimeFile(play_time_elements)) { if (!WritePlayTimeFile(play_time_elements)) {
return false; return false;
@ -86,6 +89,19 @@ u64 GetPlayTime(u64 program_id) {
return play_time_elements.at(it - play_time_elements.begin()).play_time; return play_time_elements.at(it - play_time_elements.begin()).play_time;
} }
u64 GetTotalTimes(u64 program_id) {
std::vector<PlayTimeElement> play_time_elements;
if (!ReadPlayTimeFile(play_time_elements)) {
return 0;
}
const auto it = std::find(play_time_elements.begin(), play_time_elements.end(), program_id);
if (it == play_time_elements.end()) {
return 0;
}
return play_time_elements.at(it - play_time_elements.begin()).total_times;
}
bool PlayTimeManager::ResetProgramPlayTime(u64 program_id) { bool PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
std::vector<PlayTimeElement> play_time_elements; std::vector<PlayTimeElement> play_time_elements;
@ -174,4 +190,8 @@ QString ReadablePlayTime(qulonglong time_seconds) {
.arg(QString::fromUtf8(units[unit])); .arg(QString::fromUtf8(units[unit]));
} }
QString ReadableTotalTimes(qulonglong times_count) {
return QStringLiteral("%L1").arg((double)times_count, 0, 'f', 0);
}
} // namespace PlayTime } // namespace PlayTime

View File

@ -20,6 +20,7 @@ namespace PlayTime {
struct PlayTimeElement { struct PlayTimeElement {
u64 program_id; u64 program_id;
u64 play_time; u64 play_time;
u64 total_times;
inline bool operator==(const PlayTimeElement& other) const { inline bool operator==(const PlayTimeElement& other) const {
return program_id == other.program_id; return program_id == other.program_id;
@ -49,6 +50,7 @@ public:
private: private:
u64 running_program_id; u64 running_program_id;
std::chrono::steady_clock::time_point last_timestamp; std::chrono::steady_clock::time_point last_timestamp;
u64 last_total_times;
std::jthread play_time_thread; std::jthread play_time_thread;
void AutoTimestamp(std::stop_token stop_token); void AutoTimestamp(std::stop_token stop_token);
void Save(); void Save();
@ -56,13 +58,15 @@ private:
std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(); std::optional<std::filesystem::path> GetCurrentUserPlayTimePath();
bool UpdatePlayTime(u64 program_id, u64 add_play_time); bool UpdatePlayTime(u64 program_id, u64 add_play_time, u64 add_total_times);
[[nodiscard]] bool ReadPlayTimeFile(std::vector<PlayTimeElement>& out_play_time_elements); [[nodiscard]] bool ReadPlayTimeFile(std::vector<PlayTimeElement>& out_play_time_elements);
[[nodiscard]] bool WritePlayTimeFile(const std::vector<PlayTimeElement>& play_time_elements); [[nodiscard]] bool WritePlayTimeFile(const std::vector<PlayTimeElement>& play_time_elements);
u64 GetPlayTime(u64 program_id); u64 GetPlayTime(u64 program_id);
u64 GetTotalTimes(u64 program_id);
QString ReadablePlayTime(qulonglong time_seconds); QString ReadablePlayTime(qulonglong time_seconds);
QString ReadableTotalTimes(qulonglong times_count);
} // namespace PlayTime } // namespace PlayTime

View File

@ -18,6 +18,7 @@ namespace {
struct PlayTimeElement { struct PlayTimeElement {
ProgramId program_id; ProgramId program_id;
PlayTime play_time; PlayTime play_time;
TotalTimes total_times;
}; };
std::optional<std::filesystem::path> GetCurrentUserPlayTimePath( std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
@ -57,9 +58,9 @@ std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
return false; return false;
} }
for (const auto& [program_id, play_time] : elements) { for (const auto& [program_id, play_time, total_times] : elements) {
if (program_id != 0) { if (program_id != 0) {
out_play_time_db[program_id] = play_time; out_play_time_db[program_id] = {play_time, total_times};
} }
} }
} }
@ -89,7 +90,7 @@ std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
for (auto& [program_id, play_time] : play_time_db) { for (auto& [program_id, play_time] : play_time_db) {
if (program_id != 0) { if (program_id != 0) {
elements.push_back(PlayTimeElement{program_id, play_time}); elements.push_back(PlayTimeElement{program_id, play_time.first, play_time.second});
} }
} }
@ -139,7 +140,8 @@ void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
while (!stop_token.stop_requested()) { while (!stop_token.stop_requested()) {
Common::StoppableTimedWait(stop_token, 30s); Common::StoppableTimedWait(stop_token, 30s);
database[running_program_id] += GetDuration(); database[running_program_id].first += GetDuration();
database[running_program_id].second += 1;
Save(); Save();
} }
} }
@ -153,7 +155,16 @@ void PlayTimeManager::Save() {
u64 PlayTimeManager::GetPlayTime(u64 program_id) const { u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
auto it = database.find(program_id); auto it = database.find(program_id);
if (it != database.end()) { if (it != database.end()) {
return it->second; return it->second.first;
} else {
return 0;
}
}
u64 PlayTimeManager::GetTotalTimes(u64 program_id) const {
auto it = database.find(program_id);
if (it != database.end()) {
return it->second.second;
} else { } else {
return 0; return 0;
} }
@ -179,4 +190,12 @@ QString ReadablePlayTime(qulonglong time_seconds) {
.arg(QString::fromUtf8(unit)); .arg(QString::fromUtf8(unit));
} }
QString ReadableTotalTimes(qulonglong times_count) {
if (times_count == 0) {
return {};
}
return QStringLiteral("%L1").arg((double)times_count, 0, 'f', 0);
}
} // namespace PlayTime } // namespace PlayTime

View File

@ -19,7 +19,8 @@ namespace PlayTime {
using ProgramId = u64; using ProgramId = u64;
using PlayTime = u64; using PlayTime = u64;
using PlayTimeDatabase = std::map<ProgramId, PlayTime>; using TotalTimes = u64;
using PlayTimeDatabase = std::map<ProgramId, std::pair<PlayTime, TotalTimes>>;
class PlayTimeManager { class PlayTimeManager {
public: public:
@ -30,6 +31,7 @@ public:
SUDACHI_NON_MOVEABLE(PlayTimeManager); SUDACHI_NON_MOVEABLE(PlayTimeManager);
u64 GetPlayTime(u64 program_id) const; u64 GetPlayTime(u64 program_id) const;
u64 GetTotalTimes(u64 program_id) const;
void ResetProgramPlayTime(u64 program_id); void ResetProgramPlayTime(u64 program_id);
void SetProgramId(u64 program_id); void SetProgramId(u64 program_id);
void Start(); void Start();
@ -46,5 +48,6 @@ private:
}; };
QString ReadablePlayTime(qulonglong time_seconds); QString ReadablePlayTime(qulonglong time_seconds);
QString ReadableTotalTimes(qulonglong times_count);
} // namespace PlayTime } // namespace PlayTime

View File

@ -212,6 +212,9 @@ struct Values {
// Play time // Play time
Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList}; Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList};
// Total times
Setting<bool> show_total_times{linkage, true, "show_total_times", Category::UiGameList};
bool configuration_applied; bool configuration_applied;
bool reset_to_defaults; bool reset_to_defaults;
bool shortcut_already_warned{false}; bool shortcut_already_warned{false};