Skip to content

Commit

Permalink
GameList: Add played time tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
stenzek committed Oct 21, 2022
1 parent 6def728 commit ca571f8
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 40 deletions.
24 changes: 24 additions & 0 deletions src/common/file_system.cpp
Expand Up @@ -1914,4 +1914,28 @@ bool FileSystem::SetPathCompression(const char* path, bool enable)
return false;
}

FileSystem::POSIXLock::POSIXLock(int fd)
{
if (lockf(fd, F_LOCK, 0) == 0)
{
m_fd = fd;
}
else
{
Log_ErrorPrintf("lockf() failed: %d", fd);
m_fd = -1;
}
}

FileSystem::POSIXLock::POSIXLock(std::FILE* fp)
{
POSIXLock(fileno(fp));
}

FileSystem::POSIXLock::~POSIXLock()
{
if (m_fd >= 0)
lockf(m_fd, F_ULOCK, m_fd);
}

#endif
14 changes: 14 additions & 0 deletions src/common/file_system.h
Expand Up @@ -108,6 +108,20 @@ enum class FileShareMode
ManagedCFilePtr OpenManagedSharedCFile(const char* filename, const char* mode, FileShareMode share_mode);
std::FILE* OpenSharedCFile(const char* filename, const char* mode, FileShareMode share_mode);

/// Abstracts a POSIX file lock.
#ifndef _WIN32
class POSIXLock
{
public:
POSIXLock(int fd);
POSIXLock(std::FILE* fp);
~POSIXLock();

private:
int m_fd;
};
#endif

std::optional<std::vector<u8>> ReadBinaryFile(const char* filename);
std::optional<std::vector<u8>> ReadBinaryFile(std::FILE* fp);
std::optional<std::string> ReadFileToString(const char* filename);
Expand Down
38 changes: 37 additions & 1 deletion src/duckstation-qt/gamelistmodel.cpp
Expand Up @@ -301,6 +301,17 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
case Column_Size:
return QString("%1 MB").arg(static_cast<double>(ge->total_size) / 1048576.0, 0, 'f', 2);

case Column_TimePlayed:
{
if (ge->total_played_time == 0)
return {};
else
return QtUtils::StringViewToQString(GameList::FormatTimespan(ge->total_played_time));
}

case Column_LastPlayed:
return QtUtils::StringViewToQString(GameList::FormatTimestamp(ge->last_played_time));

case Column_Cover:
{
if (m_show_titles_for_covers)
Expand Down Expand Up @@ -352,6 +363,12 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
case Column_Compatibility:
return static_cast<int>(ge->compatibility);

case Column_TimePlayed:
return static_cast<qlonglong>(ge->total_played_time);

case Column_LastPlayed:
return static_cast<qlonglong>(ge->last_played_time);

case Column_Size:
return static_cast<qulonglong>(ge->total_size);

Expand Down Expand Up @@ -534,6 +551,22 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r
return (left->release_date < right->release_date);
}

case Column_TimePlayed:
{
if (left->total_played_time == right->total_played_time)
return titlesLessThan(left_row, right_row);

return (left->total_played_time < right->total_played_time);
}

case Column_LastPlayed:
{
if (left->last_played_time == right->last_played_time)
return titlesLessThan(left_row, right_row);

return (left->last_played_time < right->last_played_time);
}

case Column_Players:
{
u8 left_players = (left->min_players << 4) + left->max_players;
Expand All @@ -558,7 +591,8 @@ void GameListModel::loadCommonImages()
m_region_pixmaps[i] = QtUtils::GetIconForRegion(static_cast<DiscRegion>(i)).pixmap(42, 30);

for (int i = 0; i < static_cast<int>(GameDatabase::CompatibilityRating::Count); i++)
m_compatibility_pixmaps[i] = QtUtils::GetIconForCompatibility(static_cast<GameDatabase::CompatibilityRating>(i)).pixmap(96, 24);
m_compatibility_pixmaps[i] =
QtUtils::GetIconForCompatibility(static_cast<GameDatabase::CompatibilityRating>(i)).pixmap(96, 24);

m_placeholder_pixmap.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()));
}
Expand All @@ -574,6 +608,8 @@ void GameListModel::setColumnDisplayNames()
m_column_display_names[Column_Genre] = tr("Genre");
m_column_display_names[Column_Year] = tr("Year");
m_column_display_names[Column_Players] = tr("Players");
m_column_display_names[Column_TimePlayed] = tr("Time Played");
m_column_display_names[Column_LastPlayed] = tr("Last Played");
m_column_display_names[Column_Size] = tr("Size");
m_column_display_names[Column_Region] = tr("Region");
m_column_display_names[Column_Compatibility] = tr("Compatibility");
Expand Down
2 changes: 2 additions & 0 deletions src/duckstation-qt/gamelistmodel.h
Expand Up @@ -26,6 +26,8 @@ class GameListModel final : public QAbstractTableModel
Column_Genre,
Column_Year,
Column_Players,
Column_TimePlayed,
Column_LastPlayed,
Column_Size,
Column_Region,
Column_Compatibility,
Expand Down
6 changes: 5 additions & 1 deletion src/duckstation-qt/gamelistwidget.cpp
Expand Up @@ -457,6 +457,8 @@ void GameListWidget::resizeTableViewColumnsToFit()
200, // genre
50, // year
100, // players
80, // time played
80, // last played
80, // size
50, // region
100 // compatibility
Expand All @@ -483,8 +485,10 @@ void GameListWidget::loadTableViewColumnVisibilitySettings()
true, // developer
false, // publisher
false, // genre
true, // year
false, // year
false, // players
true, // time played
true, // last played
true, // size
true, // region
true // compatibility
Expand Down
4 changes: 4 additions & 0 deletions src/duckstation-qt/mainwindow.cpp
Expand Up @@ -542,6 +542,10 @@ void MainWindow::onSystemDestroyed()
updateEmulationActions(false, false, Achievements::ChallengeModeActive());
switchToGameListView();

// reload played time
if (m_game_list_widget->isShowingGameList())
m_game_list_widget->refresh(false);

if (m_cheat_manager_dialog)
{
delete m_cheat_manager_dialog;
Expand Down
26 changes: 26 additions & 0 deletions src/frontend-common/common_host.cpp
Expand Up @@ -70,6 +70,8 @@
Log_SetChannel(CommonHostInterface);

namespace CommonHost {
static void UpdateSessionTime(const std::string& new_serial);

#ifdef WITH_DISCORD_PRESENCE
static void SetDiscordPresenceEnabled(bool enabled);
static void InitializeDiscordPresence();
Expand All @@ -79,6 +81,10 @@ static void PollDiscordPresence();
#endif
} // namespace CommonHost

// Used to track play time. We use a monotonic timer here, in case of clock changes.
static u64 s_session_start_time = 0;
static std::string s_session_serial;

#ifdef WITH_DISCORD_PRESENCE
// discord rich presence
bool m_discord_presence_enabled = false;
Expand Down Expand Up @@ -278,6 +284,8 @@ void CommonHost::OnGameChanged(const std::string& disc_path, const std::string&
UpdateDiscordPresence(false);
#endif

UpdateSessionTime(game_serial);

SaveStateSelectorUI::RefreshList();
}

Expand Down Expand Up @@ -374,6 +382,24 @@ void CommonHost::CheckForSettingsChanges(const Settings& old_settings)
}
}

void CommonHost::UpdateSessionTime(const std::string& new_serial)
{
if (s_session_serial == new_serial)
return;

const u64 ctime = Common::Timer::GetCurrentValue();
if (!s_session_serial.empty())
{
// round up to seconds
const std::time_t etime = static_cast<std::time_t>(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time)));
const std::time_t wtime = std::time(nullptr);
GameList::AddPlayedTimeForSerial(s_session_serial, wtime, etime);
}

s_session_serial = new_serial;
s_session_start_time = ctime;
}

void Host::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
{
InputManager::SetPadVibrationIntensity(pad_index, large_or_single_motor_intensity, small_motor_intensity);
Expand Down
7 changes: 5 additions & 2 deletions src/frontend-common/fullscreen_ui.cpp
Expand Up @@ -5029,15 +5029,14 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
ImGui::PushFont(g_medium_font);

// developer
const char* developer = "Unknown Developer";
if (!selected_entry->developer.empty())
{
text_width =
ImGui::CalcTextSize(selected_entry->developer.c_str(),
selected_entry->developer.c_str() + selected_entry->developer.length(), false, work_width)
.x;
ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
ImGui::TextWrapped("%s", developer);
ImGui::TextWrapped("%s", selected_entry->developer.c_str());
}

// code
Expand Down Expand Up @@ -5076,6 +5075,10 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
}
ImGui::Text(" (%s)", GameDatabase::GetCompatibilityRatingDisplayName(selected_entry->compatibility));

// play time
ImGui::Text("Time Played: %s", GameList::FormatTimespan(selected_entry->total_played_time).GetCharArray());
ImGui::Text("Last Played: %s", GameList::FormatTimestamp(selected_entry->last_played_time).GetCharArray());

// size
ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);

Expand Down

0 comments on commit ca571f8

Please sign in to comment.