mirror of https://github.com/PCSX2/pcsx2.git
Qt: Add play time tracking based on serials
This commit is contained in:
parent
a3b3edb565
commit
5647ca7f08
|
@ -91,7 +91,7 @@ static inline bool FileSystemCharacterIsSane(char c, bool strip_slashes)
|
|||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
template <typename T>
|
||||
static inline void PathAppendString(std::string& dst, const T& src)
|
||||
{
|
||||
if (dst.capacity() < (dst.length() + src.length()))
|
||||
|
@ -100,7 +100,7 @@ static inline void PathAppendString(std::string& dst, const T& src)
|
|||
bool last_separator = (!dst.empty() && dst.back() == FS_OSPATH_SEPARATOR_CHARACTER);
|
||||
|
||||
size_t index = 0;
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
// special case for UNC paths here
|
||||
if (dst.empty() && src.length() >= 3 && src[0] == '\\' && src[1] == '\\' && src[2] != '\\')
|
||||
|
@ -192,7 +192,8 @@ bool Path::IsAbsolute(const std::string_view& path)
|
|||
{
|
||||
#ifdef _WIN32
|
||||
return (path.length() >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) &&
|
||||
path[1] == ':' && (path[2] == '/' || path[2] == '\\')) || (path.length() >= 3 && path[0] == '\\' && path[1] == '\\');
|
||||
path[1] == ':' && (path[2] == '/' || path[2] == '\\')) ||
|
||||
(path.length() >= 3 && path[0] == '\\' && path[1] == '\\');
|
||||
#else
|
||||
return (path.length() >= 1 && path[0] == '/');
|
||||
#endif
|
||||
|
@ -202,7 +203,7 @@ std::string Path::ToNativePath(const std::string_view& path)
|
|||
{
|
||||
std::string ret;
|
||||
PathAppendString(ret, path);
|
||||
|
||||
|
||||
// remove trailing slashes
|
||||
if (ret.length() > 1)
|
||||
{
|
||||
|
@ -1930,4 +1931,36 @@ 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
|
||||
{
|
||||
Console.Error("lockf() failed: %d", errno);
|
||||
m_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
FileSystem::POSIXLock::POSIXLock(std::FILE* fp)
|
||||
{
|
||||
m_fd = fileno(fp);
|
||||
if (m_fd >= 0)
|
||||
{
|
||||
if (lockf(m_fd, F_LOCK, 0) != 0)
|
||||
{
|
||||
Console.Error("lockf() failed: %d", errno);
|
||||
m_fd = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileSystem::POSIXLock::~POSIXLock()
|
||||
{
|
||||
if (m_fd >= 0)
|
||||
lockf(m_fd, F_ULOCK, m_fd);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -164,4 +164,17 @@ namespace FileSystem
|
|||
/// Does nothing and returns false on non-Windows platforms.
|
||||
bool SetPathCompression(const char* path, bool enable);
|
||||
|
||||
/// Abstracts a POSIX file lock.
|
||||
#ifndef _WIN32
|
||||
class POSIXLock
|
||||
{
|
||||
public:
|
||||
POSIXLock(int fd);
|
||||
POSIXLock(std::FILE* fp);
|
||||
~POSIXLock();
|
||||
|
||||
private:
|
||||
int m_fd;
|
||||
};
|
||||
#endif
|
||||
}; // namespace FileSystem
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "Pcsx2Defs.h"
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
@ -59,6 +60,8 @@ namespace detail
|
|||
};
|
||||
} // namespace detail
|
||||
|
||||
// This requires C++20, so fallback to ugly heap allocations if we don't have it.
|
||||
#if __cplusplus >= 202002L
|
||||
template <typename ValueType>
|
||||
using UnorderedStringMap =
|
||||
std::unordered_map<std::string, ValueType, detail::transparent_string_hash, detail::transparent_string_equal>;
|
||||
|
@ -70,6 +73,38 @@ using UnorderedStringSet =
|
|||
using UnorderedStringMultiSet =
|
||||
std::unordered_multiset<std::string, detail::transparent_string_hash, detail::transparent_string_equal>;
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
__fi typename UnorderedStringMap<ValueType>::const_iterator
|
||||
UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key)
|
||||
{
|
||||
return map.find(key);
|
||||
}
|
||||
template <typename KeyType, typename ValueType>
|
||||
__fi typename UnorderedStringMap<ValueType>::iterator
|
||||
UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key)
|
||||
{
|
||||
return map.find(key);
|
||||
}
|
||||
#else
|
||||
template <typename ValueType>
|
||||
using UnorderedStringMap = std::unordered_map<std::string, ValueType>;
|
||||
template <typename ValueType>
|
||||
using UnorderedStringMultimap = std::unordered_multimap<std::string, ValueType>;
|
||||
using UnorderedStringSet = std::unordered_set<std::string>;
|
||||
using UnorderedStringMultiSet = std::unordered_multiset<std::string>;
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
__fi typename UnorderedStringMap<ValueType>::const_iterator UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key)
|
||||
{
|
||||
return map.find(std::string(key));
|
||||
}
|
||||
template <typename KeyType, typename ValueType>
|
||||
__fi typename UnorderedStringMap<ValueType>::iterator UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key)
|
||||
{
|
||||
return map.find(std::string(key));
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename ValueType>
|
||||
using StringMap = std::map<std::string, ValueType, detail::transparent_string_less>;
|
||||
template <typename ValueType>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
#include <QtGui/QPainter>
|
||||
|
||||
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
||||
{"Type", "Code", "Title", "File Title", "CRC", "Size", "Region", "Compatibility", "Cover"}};
|
||||
{"Type", "Code", "Title", "File Title", "CRC", "Time Played", "Last Played", "Size", "Region", "Compatibility", "Cover"}};
|
||||
|
||||
static constexpr int COVER_ART_WIDTH = 350;
|
||||
static constexpr int COVER_ART_HEIGHT = 512;
|
||||
|
@ -176,7 +176,7 @@ void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
|
|||
// while there's outstanding jobs, the old jobs won't proceed (at the wrong size), or get added into the grid.
|
||||
const u32 counter = m_cover_scale_counter.load(std::memory_order_acquire);
|
||||
|
||||
QFuture<QPixmap> future = QtConcurrent::run([this, path = ge->path, title = ge->title, serial = ge->serial, counter]()->QPixmap {
|
||||
QFuture<QPixmap> future = QtConcurrent::run([this, path = ge->path, title = ge->title, serial = ge->serial, counter]() -> QPixmap {
|
||||
QPixmap image;
|
||||
if (m_cover_scale_counter.load(std::memory_order_acquire) == counter)
|
||||
{
|
||||
|
@ -299,6 +299,17 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
|
|||
case Column_CRC:
|
||||
return QString::fromStdString(fmt::format("{:08X}", ge->crc));
|
||||
|
||||
case Column_TimePlayed:
|
||||
{
|
||||
if (ge->total_played_time == 0)
|
||||
return {};
|
||||
else
|
||||
return QString::fromStdString(GameList::FormatTimespan(ge->total_played_time, true));
|
||||
}
|
||||
|
||||
case Column_LastPlayed:
|
||||
return QString::fromStdString(GameList::FormatTimestamp(ge->last_played_time));
|
||||
|
||||
case Column_Size:
|
||||
return QString("%1 MB").arg(static_cast<double>(ge->total_size) / 1048576.0, 0, 'f', 2);
|
||||
|
||||
|
@ -335,6 +346,12 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
|
|||
case Column_CRC:
|
||||
return static_cast<int>(ge->crc);
|
||||
|
||||
case Column_TimePlayed:
|
||||
return static_cast<qlonglong>(ge->total_played_time);
|
||||
|
||||
case Column_LastPlayed:
|
||||
return static_cast<qlonglong>(ge->last_played_time);
|
||||
|
||||
case Column_Region:
|
||||
return static_cast<int>(ge->region);
|
||||
|
||||
|
@ -504,6 +521,22 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r
|
|||
return (left->crc < right->crc);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -552,6 +585,8 @@ void GameListModel::setColumnDisplayNames()
|
|||
m_column_display_names[Column_Title] = tr("Title");
|
||||
m_column_display_names[Column_FileTitle] = tr("File Title");
|
||||
m_column_display_names[Column_CRC] = tr("CRC");
|
||||
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");
|
||||
|
|
|
@ -36,6 +36,8 @@ public:
|
|||
Column_Title,
|
||||
Column_FileTitle,
|
||||
Column_CRC,
|
||||
Column_TimePlayed,
|
||||
Column_LastPlayed,
|
||||
Column_Size,
|
||||
Column_Region,
|
||||
Column_Compatibility,
|
||||
|
|
|
@ -482,6 +482,8 @@ void GameListWidget::resizeTableViewColumnsToFit()
|
|||
-1, // title
|
||||
-1, // file title
|
||||
65, // crc
|
||||
80, // time played
|
||||
80, // last played
|
||||
80, // size
|
||||
60, // region
|
||||
100 // compatibility
|
||||
|
@ -502,6 +504,8 @@ void GameListWidget::loadTableViewColumnVisibilitySettings()
|
|||
true, // title
|
||||
false, // file title
|
||||
false, // crc
|
||||
true, // time played
|
||||
true, // last played
|
||||
true, // size
|
||||
true, // region
|
||||
true // compatibility
|
||||
|
|
|
@ -1733,6 +1733,10 @@ void MainWindow::onVMStopped()
|
|||
{
|
||||
switchToGameListView();
|
||||
}
|
||||
|
||||
// reload played time
|
||||
if (m_game_list_widget->isShowingGameList())
|
||||
m_game_list_widget->refresh(false);
|
||||
}
|
||||
|
||||
void MainWindow::onGameChanged(const QString& path, const QString& serial, const QString& name, quint32 crc)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "common/CrashHandler.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/Timer.h"
|
||||
#include "common/Threading.h"
|
||||
#include "Frontend/CommonHost.h"
|
||||
#include "Frontend/FullscreenUI.h"
|
||||
|
@ -63,6 +64,8 @@ namespace CommonHost
|
|||
|
||||
static void UpdateInhibitScreensaver(bool allow);
|
||||
|
||||
static void UpdateSessionTime(const std::string& new_serial);
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
static void InitializeDiscordPresence();
|
||||
static void ShutdownDiscordPresence();
|
||||
|
@ -71,6 +74,10 @@ namespace CommonHost
|
|||
#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;
|
||||
|
||||
static bool s_screensaver_inhibited = false;
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
|
@ -361,6 +368,8 @@ void CommonHost::OnVMResumed()
|
|||
|
||||
void CommonHost::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc)
|
||||
{
|
||||
UpdateSessionTime(game_serial);
|
||||
|
||||
if (FullscreenUI::IsInitialized())
|
||||
{
|
||||
GetMTGS().RunOnGSThread([disc_path, game_serial, game_name, game_crc]() {
|
||||
|
@ -429,6 +438,30 @@ void CommonHost::UpdateInhibitScreensaver(bool inhibit)
|
|||
Console.Warning("Failed to inhibit screen saver.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
u64 CommonHost::GetSessionPlayedTime()
|
||||
{
|
||||
const u64 ctime = Common::Timer::GetCurrentValue();
|
||||
return static_cast<u64>(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time)));
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
|
||||
void CommonHost::InitializeDiscordPresence()
|
||||
|
|
|
@ -76,6 +76,9 @@ namespace CommonHost
|
|||
/// Provided by the host; called once per frame at guest vsync.
|
||||
void CPUThreadVSync();
|
||||
|
||||
/// Returns the time elapsed in the current play session.
|
||||
u64 GetSessionPlayedTime();
|
||||
|
||||
#ifdef ENABLE_DISCORD_PRESENCE
|
||||
/// Called when the rich presence string, provided by RetroAchievements, changes.
|
||||
void UpdateDiscordPresence(const std::string& rich_presence);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
#include "Frontend/CommonHost.h"
|
||||
#include "Frontend/FullscreenUI.h"
|
||||
#include "Frontend/ImGuiManager.h"
|
||||
#include "Frontend/ImGuiFullscreen.h"
|
||||
|
@ -3600,6 +3601,12 @@ void FullscreenUI::DrawGameFixesSettingsPage()
|
|||
EndMenuButtons();
|
||||
}
|
||||
|
||||
static void DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text, const char* text_end = nullptr)
|
||||
{
|
||||
dl->AddText(font, font->FontSize, pos + LayoutScale(1.0f, 1.0f), IM_COL32(0, 0, 0, 100), text, text_end);
|
||||
dl->AddText(font, font->FontSize, pos, col, text, text_end);
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawPauseMenu(MainWindowType type)
|
||||
{
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
@ -3629,13 +3636,13 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
|
|||
|
||||
float rp_height = 0.0f;
|
||||
|
||||
dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), s_current_game_title.c_str());
|
||||
DrawShadowedText(dl, g_large_font, title_pos, IM_COL32(255, 255, 255, 255), s_current_game_title.c_str());
|
||||
if (!path_string.empty())
|
||||
{
|
||||
dl->AddText(g_medium_font, g_medium_font->FontSize, path_pos, IM_COL32(255, 255, 255, 255), path_string.data(),
|
||||
DrawShadowedText(dl, g_medium_font, path_pos, IM_COL32(255, 255, 255, 255), path_string.data(),
|
||||
path_string.data() + path_string.length());
|
||||
}
|
||||
dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), s_current_game_subtitle.c_str());
|
||||
DrawShadowedText(dl, g_medium_font, subtitle_pos, IM_COL32(255, 255, 255, 255), s_current_game_subtitle.c_str());
|
||||
|
||||
|
||||
HostDisplayTexture* const cover = GetCoverForCurrentGame();
|
||||
|
@ -3647,6 +3654,41 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
|
|||
dl->AddImage(cover->GetHandle(), image_rect.Min, image_rect.Max);
|
||||
}
|
||||
|
||||
// current time / play time
|
||||
{
|
||||
char buf[256];
|
||||
struct tm ltime;
|
||||
const std::time_t ctime(std::time(nullptr));
|
||||
#ifdef _MSC_VER
|
||||
localtime_s(<ime, &ctime);
|
||||
#else
|
||||
localtime_r(&ctime, <ime);
|
||||
#endif
|
||||
std::strftime(buf, sizeof(buf), "%X", <ime);
|
||||
|
||||
const ImVec2 time_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, buf));
|
||||
const ImVec2 time_pos(display_size.x - LayoutScale(10.0f) - time_size.x, LayoutScale(10.0f));
|
||||
DrawShadowedText(dl, g_large_font, time_pos, IM_COL32(255, 255, 255, 255), buf);
|
||||
|
||||
if (!s_current_game_serial.empty())
|
||||
{
|
||||
const std::time_t cached_played_time = GameList::GetCachedPlayedTimeForSerial(s_current_game_serial);
|
||||
const std::time_t session_time = static_cast<std::time_t>(CommonHost::GetSessionPlayedTime());
|
||||
const std::string played_time_str(GameList::FormatTimespan(cached_played_time + session_time, true));
|
||||
const std::string session_time_str(GameList::FormatTimespan(session_time, true));
|
||||
|
||||
std::snprintf(buf, std::size(buf), "This Session: %s", session_time_str.c_str());
|
||||
const ImVec2 session_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, buf));
|
||||
const ImVec2 session_pos(display_size.x - LayoutScale(10.0f) - session_size.x, time_pos.y + g_large_font->FontSize + LayoutScale(4.0f));
|
||||
DrawShadowedText(dl, g_medium_font, session_pos, IM_COL32(255, 255, 255, 255), buf);
|
||||
|
||||
std::snprintf(buf, std::size(buf), "All Time: %s", played_time_str.c_str());
|
||||
const ImVec2 total_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, buf));
|
||||
const ImVec2 total_pos(display_size.x - LayoutScale(10.0f) - total_size.x, session_pos.y + g_medium_font->FontSize + LayoutScale(4.0f));
|
||||
DrawShadowedText(dl, g_medium_font, total_pos, IM_COL32(255, 255, 255, 255), buf);
|
||||
}
|
||||
}
|
||||
|
||||
const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT));
|
||||
const ImVec2 window_pos(0.0f, display_size.y - window_size.y);
|
||||
|
||||
|
@ -4395,6 +4437,10 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
|
|||
}
|
||||
ImGui::Text(" (%s)", GameList::EntryCompatibilityRatingToString(selected_entry->compatibility_rating));
|
||||
|
||||
// play time
|
||||
ImGui::Text("Time Played: %s", GameList::FormatTimespan(selected_entry->total_played_time).c_str());
|
||||
ImGui::Text("Last Played: %s", GameList::FormatTimestamp(selected_entry->last_played_time).c_str());
|
||||
|
||||
// size
|
||||
ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "common/Assertions.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/HeterogeneousContainers.h"
|
||||
#include "common/HTTPDownloader.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/ProgressCallback.h"
|
||||
|
@ -37,15 +38,32 @@
|
|||
#include "Elfheader.h"
|
||||
#include "VMManager.h"
|
||||
|
||||
enum : u32
|
||||
{
|
||||
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
||||
GAME_LIST_CACHE_VERSION = 32
|
||||
};
|
||||
#ifdef _WIN32
|
||||
#include "common/RedtapeWindows.h"
|
||||
#endif
|
||||
|
||||
namespace GameList
|
||||
{
|
||||
using CacheMap = std::unordered_map<std::string, GameList::Entry>;
|
||||
enum : u32
|
||||
{
|
||||
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
||||
GAME_LIST_CACHE_VERSION = 32,
|
||||
|
||||
|
||||
PLAYED_TIME_SERIAL_LENGTH = 32,
|
||||
PLAYED_TIME_LAST_TIME_LENGTH = 20, // uint64
|
||||
PLAYED_TIME_TOTAL_TIME_LENGTH = 20, // uint64
|
||||
PLAYED_TIME_LINE_LENGTH = PLAYED_TIME_SERIAL_LENGTH + 1 + PLAYED_TIME_LAST_TIME_LENGTH + 1 + PLAYED_TIME_TOTAL_TIME_LENGTH,
|
||||
};
|
||||
|
||||
struct PlayedTimeEntry
|
||||
{
|
||||
std::time_t last_played_time;
|
||||
std::time_t total_played_time;
|
||||
};
|
||||
|
||||
using CacheMap = UnorderedStringMap<Entry>;
|
||||
using PlayedTimeMap = UnorderedStringMap<PlayedTimeEntry>;
|
||||
|
||||
static bool IsScannableFilename(const std::string_view& path);
|
||||
|
||||
|
@ -55,10 +73,11 @@ namespace GameList
|
|||
static bool GetIsoListEntry(const std::string& path, GameList::Entry* entry);
|
||||
|
||||
static bool GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry);
|
||||
static void ScanDirectory(
|
||||
const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, ProgressCallback* progress);
|
||||
static bool AddFileFromCache(const std::string& path, std::time_t timestamp);
|
||||
static bool ScanFile(std::string path, std::time_t timestamp);
|
||||
static void ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths,
|
||||
const PlayedTimeMap& played_time_map, ProgressCallback* progress);
|
||||
static bool AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map);
|
||||
static bool ScanFile(
|
||||
std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock, const PlayedTimeMap& played_time_map);
|
||||
|
||||
static void LoadCache();
|
||||
static bool LoadEntriesFromCache(std::FILE* stream);
|
||||
|
@ -67,20 +86,18 @@ namespace GameList
|
|||
static void CloseCacheFileStream();
|
||||
static void DeleteCacheFile();
|
||||
|
||||
static void LoadDatabase();
|
||||
static std::string GetPlayedTimeFile();
|
||||
static bool ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEntry& entry);
|
||||
static std::string MakePlayedTimeLine(const std::string& serial, const PlayedTimeEntry& entry);
|
||||
static PlayedTimeMap LoadPlayedTimeMap(const std::string& path);
|
||||
static PlayedTimeEntry UpdatePlayedTimeFile(
|
||||
const std::string& path, const std::string& serial, std::time_t last_time, std::time_t add_time);
|
||||
} // namespace GameList
|
||||
|
||||
static std::vector<GameList::Entry> m_entries;
|
||||
static std::vector<GameList::Entry> s_entries;
|
||||
static std::recursive_mutex s_mutex;
|
||||
static GameList::CacheMap m_cache_map;
|
||||
static std::FILE* m_cache_write_stream = nullptr;
|
||||
|
||||
static bool m_game_list_loaded = false;
|
||||
|
||||
bool GameList::IsGameListLoaded()
|
||||
{
|
||||
return m_game_list_loaded;
|
||||
}
|
||||
static GameList::CacheMap s_cache_map;
|
||||
static std::FILE* s_cache_write_stream = nullptr;
|
||||
|
||||
const char* GameList::EntryTypeToString(EntryType type)
|
||||
{
|
||||
|
@ -308,12 +325,12 @@ bool GameList::PopulateEntryFromPath(const std::string& path, GameList::Entry* e
|
|||
|
||||
bool GameList::GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry)
|
||||
{
|
||||
auto iter = m_cache_map.find(path);
|
||||
if (iter == m_cache_map.end())
|
||||
auto iter = UnorderedStringMapFind(s_cache_map, path);
|
||||
if (iter == s_cache_map.end())
|
||||
return false;
|
||||
|
||||
*entry = std::move(iter->second);
|
||||
m_cache_map.erase(iter);
|
||||
s_cache_map.erase(iter);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -406,11 +423,11 @@ bool GameList::LoadEntriesFromCache(std::FILE* stream)
|
|||
ge.compatibility_rating = static_cast<CompatibilityRating>(compatibility_rating);
|
||||
ge.last_modified_time = static_cast<std::time_t>(last_modified_time);
|
||||
|
||||
auto iter = m_cache_map.find(ge.path);
|
||||
if (iter != m_cache_map.end())
|
||||
auto iter = UnorderedStringMapFind(s_cache_map, ge.path);
|
||||
if (iter != s_cache_map.end())
|
||||
iter->second = std::move(ge);
|
||||
else
|
||||
m_cache_map.emplace(std::move(path), std::move(ge));
|
||||
s_cache_map.emplace(std::move(path), std::move(ge));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -432,7 +449,7 @@ void GameList::LoadCache()
|
|||
{
|
||||
Console.Warning("Deleting corrupted cache file '%s'", cache_filename.c_str());
|
||||
stream.reset();
|
||||
m_cache_map.clear();
|
||||
s_cache_map.clear();
|
||||
DeleteCacheFile();
|
||||
return;
|
||||
}
|
||||
|
@ -444,36 +461,36 @@ bool GameList::OpenCacheForWriting()
|
|||
if (cache_filename.empty())
|
||||
return false;
|
||||
|
||||
pxAssert(!m_cache_write_stream);
|
||||
m_cache_write_stream = FileSystem::OpenCFile(cache_filename.c_str(), "r+b");
|
||||
if (m_cache_write_stream)
|
||||
pxAssert(!s_cache_write_stream);
|
||||
s_cache_write_stream = FileSystem::OpenCFile(cache_filename.c_str(), "r+b");
|
||||
if (s_cache_write_stream)
|
||||
{
|
||||
// check the header
|
||||
u32 signature, version;
|
||||
if (ReadU32(m_cache_write_stream, &signature) && signature == GAME_LIST_CACHE_SIGNATURE &&
|
||||
ReadU32(m_cache_write_stream, &version) && version == GAME_LIST_CACHE_VERSION &&
|
||||
FileSystem::FSeek64(m_cache_write_stream, 0, SEEK_END) == 0)
|
||||
if (ReadU32(s_cache_write_stream, &signature) && signature == GAME_LIST_CACHE_SIGNATURE &&
|
||||
ReadU32(s_cache_write_stream, &version) && version == GAME_LIST_CACHE_VERSION &&
|
||||
FileSystem::FSeek64(s_cache_write_stream, 0, SEEK_END) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::fclose(m_cache_write_stream);
|
||||
std::fclose(s_cache_write_stream);
|
||||
}
|
||||
|
||||
Console.WriteLn("Creating new game list cache file: '%s'", cache_filename.c_str());
|
||||
|
||||
m_cache_write_stream = FileSystem::OpenCFile(cache_filename.c_str(), "w+b");
|
||||
if (!m_cache_write_stream)
|
||||
s_cache_write_stream = FileSystem::OpenCFile(cache_filename.c_str(), "w+b");
|
||||
if (!s_cache_write_stream)
|
||||
return false;
|
||||
|
||||
|
||||
// new cache file, write header
|
||||
if (!WriteU32(m_cache_write_stream, GAME_LIST_CACHE_SIGNATURE) ||
|
||||
!WriteU32(m_cache_write_stream, GAME_LIST_CACHE_VERSION))
|
||||
if (!WriteU32(s_cache_write_stream, GAME_LIST_CACHE_SIGNATURE) ||
|
||||
!WriteU32(s_cache_write_stream, GAME_LIST_CACHE_VERSION))
|
||||
{
|
||||
Console.Error("Failed to write game list cache header");
|
||||
std::fclose(m_cache_write_stream);
|
||||
m_cache_write_stream = nullptr;
|
||||
std::fclose(s_cache_write_stream);
|
||||
s_cache_write_stream = nullptr;
|
||||
FileSystem::DeleteFilePath(cache_filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
@ -484,35 +501,35 @@ bool GameList::OpenCacheForWriting()
|
|||
bool GameList::WriteEntryToCache(const Entry* entry)
|
||||
{
|
||||
bool result = true;
|
||||
result &= WriteString(m_cache_write_stream, entry->path);
|
||||
result &= WriteString(m_cache_write_stream, entry->serial);
|
||||
result &= WriteString(m_cache_write_stream, entry->title);
|
||||
result &= WriteU8(m_cache_write_stream, static_cast<u8>(entry->type));
|
||||
result &= WriteU8(m_cache_write_stream, static_cast<u8>(entry->region));
|
||||
result &= WriteU64(m_cache_write_stream, entry->total_size);
|
||||
result &= WriteU64(m_cache_write_stream, static_cast<u64>(entry->last_modified_time));
|
||||
result &= WriteU32(m_cache_write_stream, entry->crc);
|
||||
result &= WriteU8(m_cache_write_stream, static_cast<u8>(entry->compatibility_rating));
|
||||
result &= WriteString(s_cache_write_stream, entry->path);
|
||||
result &= WriteString(s_cache_write_stream, entry->serial);
|
||||
result &= WriteString(s_cache_write_stream, entry->title);
|
||||
result &= WriteU8(s_cache_write_stream, static_cast<u8>(entry->type));
|
||||
result &= WriteU8(s_cache_write_stream, static_cast<u8>(entry->region));
|
||||
result &= WriteU64(s_cache_write_stream, entry->total_size);
|
||||
result &= WriteU64(s_cache_write_stream, static_cast<u64>(entry->last_modified_time));
|
||||
result &= WriteU32(s_cache_write_stream, entry->crc);
|
||||
result &= WriteU8(s_cache_write_stream, static_cast<u8>(entry->compatibility_rating));
|
||||
|
||||
// flush after each entry, that way we don't end up with a corrupted file if we crash scanning.
|
||||
if (result)
|
||||
result = (std::fflush(m_cache_write_stream) == 0);
|
||||
result = (std::fflush(s_cache_write_stream) == 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void GameList::CloseCacheFileStream()
|
||||
{
|
||||
if (!m_cache_write_stream)
|
||||
if (!s_cache_write_stream)
|
||||
return;
|
||||
|
||||
std::fclose(m_cache_write_stream);
|
||||
m_cache_write_stream = nullptr;
|
||||
std::fclose(s_cache_write_stream);
|
||||
s_cache_write_stream = nullptr;
|
||||
}
|
||||
|
||||
void GameList::DeleteCacheFile()
|
||||
{
|
||||
pxAssert(!m_cache_write_stream);
|
||||
pxAssert(!s_cache_write_stream);
|
||||
|
||||
const std::string cache_filename(GetCacheFilename());
|
||||
if (cache_filename.empty() || !FileSystem::FileExists(cache_filename.c_str()))
|
||||
|
@ -529,7 +546,8 @@ static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const
|
|||
return (std::find(excluded_paths.begin(), excluded_paths.end(), path) != excluded_paths.end());
|
||||
}
|
||||
|
||||
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, ProgressCallback* progress)
|
||||
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
|
||||
ProgressCallback* progress)
|
||||
{
|
||||
Console.WriteLn("Scanning %s%s", path, recursive ? " (recursively)" : "");
|
||||
|
||||
|
@ -556,19 +574,15 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
|||
continue;
|
||||
}
|
||||
|
||||
std::unique_lock lock(s_mutex);
|
||||
if (GetEntryForPath(ffd.FileName.c_str()) ||
|
||||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
|
||||
{
|
||||
std::unique_lock lock(s_mutex);
|
||||
if (GetEntryForPath(ffd.FileName.c_str()) ||
|
||||
AddFileFromCache(ffd.FileName, ffd.ModificationTime) ||
|
||||
only_cache)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ownership of fp is transferred
|
||||
progress->SetFormattedStatusText("Scanning '%s'...", FileSystem::GetDisplayNameFromPath(ffd.FileName).c_str());
|
||||
ScanFile(std::move(ffd.FileName), ffd.ModificationTime);
|
||||
ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map);
|
||||
progress->SetProgressValue(files_scanned);
|
||||
}
|
||||
|
||||
|
@ -576,24 +590,29 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
|||
progress->PopState();
|
||||
}
|
||||
|
||||
bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp)
|
||||
bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map)
|
||||
{
|
||||
if (std::any_of(m_entries.begin(), m_entries.end(), [&path](const Entry& other) { return other.path == path; }))
|
||||
{
|
||||
// already exists
|
||||
return true;
|
||||
}
|
||||
|
||||
Entry entry;
|
||||
if (!GetGameListEntryFromCache(path, &entry) || entry.last_modified_time != timestamp)
|
||||
return false;
|
||||
|
||||
m_entries.push_back(std::move(entry));
|
||||
auto iter = UnorderedStringMapFind(played_time_map, entry.serial);
|
||||
if (iter != played_time_map.end())
|
||||
{
|
||||
entry.last_played_time = iter->second.last_played_time;
|
||||
entry.total_played_time = iter->second.total_played_time;
|
||||
}
|
||||
|
||||
s_entries.push_back(std::move(entry));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GameList::ScanFile(std::string path, std::time_t timestamp)
|
||||
bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
|
||||
const PlayedTimeMap& played_time_map)
|
||||
{
|
||||
// don't block UI while scanning
|
||||
lock.unlock();
|
||||
|
||||
DevCon.WriteLn("Scanning '%s'...", path.c_str());
|
||||
|
||||
Entry entry;
|
||||
|
@ -603,14 +622,21 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp)
|
|||
entry.path = std::move(path);
|
||||
entry.last_modified_time = timestamp;
|
||||
|
||||
if (m_cache_write_stream || OpenCacheForWriting())
|
||||
if (s_cache_write_stream || OpenCacheForWriting())
|
||||
{
|
||||
if (!WriteEntryToCache(&entry))
|
||||
Console.Warning("Failed to write entry '%s' to cache", entry.path.c_str());
|
||||
}
|
||||
|
||||
std::unique_lock lock(s_mutex);
|
||||
m_entries.push_back(std::move(entry));
|
||||
auto iter = UnorderedStringMapFind(played_time_map, entry.serial);
|
||||
if (iter != played_time_map.end())
|
||||
{
|
||||
entry.last_played_time = iter->second.last_played_time;
|
||||
entry.total_played_time = iter->second.total_played_time;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
s_entries.push_back(std::move(entry));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -621,13 +647,13 @@ std::unique_lock<std::recursive_mutex> GameList::GetLock()
|
|||
|
||||
const GameList::Entry* GameList::GetEntryByIndex(u32 index)
|
||||
{
|
||||
return (index < m_entries.size()) ? &m_entries[index] : nullptr;
|
||||
return (index < s_entries.size()) ? &s_entries[index] : nullptr;
|
||||
}
|
||||
|
||||
const GameList::Entry* GameList::GetEntryForPath(const char* path)
|
||||
{
|
||||
const size_t path_length = std::strlen(path);
|
||||
for (const Entry& entry : m_entries)
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.path.size() == path_length && StringUtil::Strcasecmp(entry.path.c_str(), path) == 0)
|
||||
return &entry;
|
||||
|
@ -638,7 +664,7 @@ const GameList::Entry* GameList::GetEntryForPath(const char* path)
|
|||
|
||||
const GameList::Entry* GameList::GetEntryByCRC(u32 crc)
|
||||
{
|
||||
for (const Entry& entry : m_entries)
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.crc == crc)
|
||||
return &entry;
|
||||
|
@ -649,7 +675,7 @@ const GameList::Entry* GameList::GetEntryByCRC(u32 crc)
|
|||
|
||||
const GameList::Entry* GameList::GetEntryBySerialAndCRC(const std::string_view& serial, u32 crc)
|
||||
{
|
||||
for (const Entry& entry : m_entries)
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.crc == crc && StringUtil::compareNoCase(entry.serial, serial))
|
||||
return &entry;
|
||||
|
@ -661,7 +687,7 @@ const GameList::Entry* GameList::GetEntryBySerialAndCRC(const std::string_view&
|
|||
GameList::Entry* GameList::GetMutableEntryForPath(const char* path)
|
||||
{
|
||||
const size_t path_length = std::strlen(path);
|
||||
for (Entry& entry : m_entries)
|
||||
for (Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.path.size() == path_length && StringUtil::Strcasecmp(entry.path.c_str(), path) == 0)
|
||||
return &entry;
|
||||
|
@ -672,13 +698,11 @@ GameList::Entry* GameList::GetMutableEntryForPath(const char* path)
|
|||
|
||||
u32 GameList::GetEntryCount()
|
||||
{
|
||||
return static_cast<u32>(m_entries.size());
|
||||
return static_cast<u32>(s_entries.size());
|
||||
}
|
||||
|
||||
void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* progress /* = nullptr */)
|
||||
{
|
||||
m_game_list_loaded = true;
|
||||
|
||||
if (!progress)
|
||||
progress = ProgressCallback::NullProgressCallback;
|
||||
|
||||
|
@ -691,12 +715,13 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
std::vector<Entry> old_entries;
|
||||
{
|
||||
std::unique_lock lock(s_mutex);
|
||||
old_entries.swap(m_entries);
|
||||
old_entries.swap(s_entries);
|
||||
}
|
||||
|
||||
const std::vector<std::string> excluded_paths(Host::GetStringListSetting("GameList", "ExcludedPaths"));
|
||||
const std::vector<std::string> dirs(Host::GetStringListSetting("GameList", "Paths"));
|
||||
const std::vector<std::string> recursive_dirs(Host::GetStringListSetting("GameList", "RecursivePaths"));
|
||||
const std::vector<std::string> excluded_paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths"));
|
||||
const std::vector<std::string> dirs(Host::GetBaseStringListSetting("GameList", "Paths"));
|
||||
const std::vector<std::string> recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths"));
|
||||
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
|
||||
|
||||
if (!dirs.empty() || !recursive_dirs.empty())
|
||||
{
|
||||
|
@ -710,7 +735,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
if (progress->IsCancelled())
|
||||
break;
|
||||
|
||||
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, progress);
|
||||
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, progress);
|
||||
progress->SetProgressValue(++directory_counter);
|
||||
}
|
||||
for (const std::string& dir : recursive_dirs)
|
||||
|
@ -718,14 +743,277 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
if (progress->IsCancelled())
|
||||
break;
|
||||
|
||||
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, progress);
|
||||
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, progress);
|
||||
progress->SetProgressValue(++directory_counter);
|
||||
}
|
||||
}
|
||||
|
||||
// don't need unused cache entries
|
||||
CloseCacheFileStream();
|
||||
m_cache_map.clear();
|
||||
s_cache_map.clear();
|
||||
}
|
||||
|
||||
std::string GameList::GetPlayedTimeFile()
|
||||
{
|
||||
return Path::Combine(EmuFolders::Settings, "playtime.dat");
|
||||
}
|
||||
|
||||
bool GameList::ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEntry& entry)
|
||||
{
|
||||
size_t len = std::strlen(line);
|
||||
if (len != (PLAYED_TIME_LINE_LENGTH + 1)) // \n
|
||||
{
|
||||
Console.Warning("(ParsePlayedTimeLine) Malformed line: '%s'", line);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string_view serial_tok(StringUtil::StripWhitespace(std::string_view(line, PLAYED_TIME_SERIAL_LENGTH)));
|
||||
const std::string_view total_played_time_tok(
|
||||
StringUtil::StripWhitespace(std::string_view(line + PLAYED_TIME_SERIAL_LENGTH + 1, PLAYED_TIME_LAST_TIME_LENGTH)));
|
||||
const std::string_view last_played_time_tok(StringUtil::StripWhitespace(std::string_view(
|
||||
line + PLAYED_TIME_SERIAL_LENGTH + 1 + PLAYED_TIME_LAST_TIME_LENGTH + 1, PLAYED_TIME_TOTAL_TIME_LENGTH)));
|
||||
|
||||
const std::optional<u64> total_played_time(StringUtil::FromChars<u64>(total_played_time_tok));
|
||||
const std::optional<u64> last_played_time(StringUtil::FromChars<u64>(last_played_time_tok));
|
||||
if (serial_tok.empty() || !last_played_time.has_value() || !total_played_time.has_value())
|
||||
{
|
||||
Console.Warning("(ParsePlayedTimeLine) Malformed line: '%s'", line);
|
||||
return false;
|
||||
}
|
||||
|
||||
serial = serial_tok;
|
||||
entry.last_played_time = static_cast<std::time_t>(last_played_time.value());
|
||||
entry.total_played_time = static_cast<std::time_t>(total_played_time.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GameList::MakePlayedTimeLine(const std::string& serial, const PlayedTimeEntry& entry)
|
||||
{
|
||||
return fmt::format("{:<{}} {:<{}} {:<{}}\n", serial, static_cast<unsigned>(PLAYED_TIME_SERIAL_LENGTH),
|
||||
entry.total_played_time, static_cast<unsigned>(PLAYED_TIME_TOTAL_TIME_LENGTH),
|
||||
entry.last_played_time, static_cast<unsigned>(PLAYED_TIME_LAST_TIME_LENGTH));
|
||||
}
|
||||
|
||||
GameList::PlayedTimeMap GameList::LoadPlayedTimeMap(const std::string& path)
|
||||
{
|
||||
PlayedTimeMap ret;
|
||||
|
||||
// Use write mode here, even though we're not writing, so we can lock the file from other updates.
|
||||
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
|
||||
|
||||
#ifdef _WIN32
|
||||
// On Windows, the file is implicitly locked.
|
||||
while (!fp && GetLastError() == ERROR_SHARING_VIOLATION)
|
||||
{
|
||||
Sleep(10);
|
||||
fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (fp)
|
||||
{
|
||||
#ifndef _WIN32
|
||||
FileSystem::POSIXLock flock(fp.get());
|
||||
#endif
|
||||
|
||||
char line[256];
|
||||
while (std::fgets(line, sizeof(line), fp.get()))
|
||||
{
|
||||
std::string serial;
|
||||
PlayedTimeEntry entry;
|
||||
if (!ParsePlayedTimeLine(line, serial, entry))
|
||||
continue;
|
||||
|
||||
if (UnorderedStringMapFind(ret, serial) != ret.end())
|
||||
{
|
||||
Console.Warning("(LoadPlayedTimeMap) Duplicate entry: '%s'", serial.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
ret.emplace(std::move(serial), entry);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
GameList::PlayedTimeEntry GameList::UpdatePlayedTimeFile(const std::string& path, const std::string& serial,
|
||||
std::time_t last_time, std::time_t add_time)
|
||||
{
|
||||
const PlayedTimeEntry new_entry{ last_time, add_time };
|
||||
|
||||
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
|
||||
|
||||
#ifdef _WIN32
|
||||
// On Windows, the file is implicitly locked.
|
||||
while (!fp && GetLastError() == ERROR_SHARING_VIOLATION)
|
||||
{
|
||||
Sleep(10);
|
||||
fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Doesn't exist? Create it.
|
||||
if (!fp && errno == ENOENT)
|
||||
fp = FileSystem::OpenManagedCFile(path.c_str(), "w+b");
|
||||
|
||||
if (!fp)
|
||||
{
|
||||
Console.Error("Failed to open '%s' for update.", path.c_str());
|
||||
return new_entry;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
FileSystem::POSIXLock flock(fp.get());
|
||||
#endif
|
||||
|
||||
for (;;)
|
||||
{
|
||||
char line[256];
|
||||
const s64 line_pos = FileSystem::FTell64(fp.get());
|
||||
if (!std::fgets(line, sizeof(line), fp.get()))
|
||||
break;
|
||||
|
||||
std::string line_serial;
|
||||
PlayedTimeEntry line_entry;
|
||||
if (!ParsePlayedTimeLine(line, line_serial, line_entry))
|
||||
continue;
|
||||
|
||||
if (line_serial != serial)
|
||||
continue;
|
||||
|
||||
// found it!
|
||||
line_entry.last_played_time = last_time;
|
||||
line_entry.total_played_time += add_time;
|
||||
|
||||
std::string new_line(MakePlayedTimeLine(serial, line_entry));
|
||||
if (FileSystem::FSeek64(fp.get(), line_pos, SEEK_SET) != 0 ||
|
||||
std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1 ||
|
||||
std::fflush(fp.get()) != 0)
|
||||
{
|
||||
Console.Error("Failed to update '%s'.", path.c_str());
|
||||
}
|
||||
|
||||
return line_entry;
|
||||
}
|
||||
|
||||
// new entry.
|
||||
std::string new_line(MakePlayedTimeLine(serial, new_entry));
|
||||
if (FileSystem::FSeek64(fp.get(), 0, SEEK_END) != 0 ||
|
||||
std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1)
|
||||
{
|
||||
Console.Error("Failed to write '%s'.", path.c_str());
|
||||
}
|
||||
|
||||
return new_entry;
|
||||
}
|
||||
|
||||
void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time)
|
||||
{
|
||||
if (serial.empty())
|
||||
return;
|
||||
|
||||
const PlayedTimeEntry pt(UpdatePlayedTimeFile(GetPlayedTimeFile(), serial, last_time, add_time));
|
||||
Console.WriteLn("Add %u seconds play time to %s -> now %u", static_cast<unsigned>(add_time), serial.c_str(),
|
||||
static_cast<unsigned>(pt.total_played_time));
|
||||
|
||||
std::unique_lock<std::recursive_mutex> lock(s_mutex);
|
||||
for (GameList::Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.serial != serial)
|
||||
continue;
|
||||
|
||||
entry.last_played_time = pt.last_played_time;
|
||||
entry.total_played_time = pt.total_played_time;
|
||||
}
|
||||
}
|
||||
|
||||
std::time_t GameList::GetCachedPlayedTimeForSerial(const std::string& serial)
|
||||
{
|
||||
if (serial.empty())
|
||||
return 0;
|
||||
|
||||
std::unique_lock<std::recursive_mutex> lock(s_mutex);
|
||||
for (GameList::Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.serial == serial)
|
||||
return entry.total_played_time;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string GameList::FormatTimestamp(std::time_t timestamp)
|
||||
{
|
||||
// TODO: All these strings should be translateable.
|
||||
std::string ret;
|
||||
|
||||
if (timestamp == 0)
|
||||
{
|
||||
ret = "Never";
|
||||
}
|
||||
else
|
||||
{
|
||||
struct tm ctime = {};
|
||||
struct tm ttime = {};
|
||||
const std::time_t ctimestamp = std::time(nullptr);
|
||||
#ifdef _MSC_VER
|
||||
localtime_s(&ctime, &ctimestamp);
|
||||
localtime_s(&ttime, ×tamp);
|
||||
#else
|
||||
localtime_r(&ctimestamp, &ctime);
|
||||
localtime_r(×tamp, &ttime);
|
||||
#endif
|
||||
|
||||
if (ctime.tm_year == ttime.tm_year && ctime.tm_yday == ttime.tm_yday)
|
||||
{
|
||||
ret = "Today";
|
||||
}
|
||||
else if ((ctime.tm_year == ttime.tm_year && ctime.tm_yday == (ttime.tm_yday + 1)) ||
|
||||
(ctime.tm_yday == 0 && (ctime.tm_year - 1) == ttime.tm_year))
|
||||
{
|
||||
ret = "Yesterday";
|
||||
}
|
||||
else
|
||||
{
|
||||
char buf[128];
|
||||
std::strftime(buf, std::size(buf), "%x", &ttime);
|
||||
ret.assign(buf);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string GameList::FormatTimespan(std::time_t timespan, bool long_format)
|
||||
{
|
||||
const u32 hours = static_cast<u32>(timespan / 3600);
|
||||
const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
|
||||
const u32 seconds = static_cast<u32>((timespan % 3600) % 60);
|
||||
|
||||
std::string ret;
|
||||
if (!long_format)
|
||||
{
|
||||
if (hours >= 100)
|
||||
ret = fmt::format("{}h {}m", hours, minutes);
|
||||
else if (hours > 0)
|
||||
ret = fmt::format("{}h {}m {}s", hours, minutes, seconds);
|
||||
else if (minutes > 0)
|
||||
ret = fmt::format("{}m {}s", minutes, seconds);
|
||||
else if (seconds > 0)
|
||||
ret = fmt::format("{}s", seconds);
|
||||
else
|
||||
ret = "None";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hours > 0)
|
||||
ret = fmt::format("{} hours", hours);
|
||||
else
|
||||
ret = fmt::format("{} minutes", minutes);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string GameList::GetCoverImagePathForEntry(const Entry* entry)
|
||||
|
@ -825,7 +1113,7 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
|
|||
std::vector<std::pair<std::string, std::string>> download_urls;
|
||||
{
|
||||
std::unique_lock lock(s_mutex);
|
||||
for (const GameList::Entry& entry : m_entries)
|
||||
for (const GameList::Entry& entry : s_entries)
|
||||
{
|
||||
const std::string existing_path(GetCoverImagePathForEntry(&entry));
|
||||
if (!existing_path.empty())
|
||||
|
|
|
@ -87,6 +87,8 @@ namespace GameList
|
|||
std::string title;
|
||||
u64 total_size = 0;
|
||||
std::time_t last_modified_time = 0;
|
||||
std::time_t last_played_time = 0;
|
||||
std::time_t total_played_time = 0;
|
||||
|
||||
u32 crc = 0;
|
||||
|
||||
|
@ -115,13 +117,23 @@ namespace GameList
|
|||
const Entry* GetEntryBySerialAndCRC(const std::string_view& serial, u32 crc);
|
||||
u32 GetEntryCount();
|
||||
|
||||
bool IsGameListLoaded();
|
||||
|
||||
/// Populates the game list with files in the configured directories.
|
||||
/// If invalidate_cache is set, all files will be re-scanned.
|
||||
/// If only_cache is set, no new files will be scanned, only those present in the cache.
|
||||
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);
|
||||
|
||||
/// Add played time for the specified serial.
|
||||
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
|
||||
|
||||
/// Returns the total time played for a game. Requires the game to be scanned in the list.
|
||||
std::time_t GetCachedPlayedTimeForSerial(const std::string& serial);
|
||||
|
||||
/// Formats a timestamp to something human readable (e.g. Today, Yesterday, 10/11/12).
|
||||
std::string FormatTimestamp(std::time_t timestamp);
|
||||
|
||||
/// Formats a timespan to something human readable (e.g. 1h2m3s or 1 hour).
|
||||
std::string FormatTimespan(std::time_t timespan, bool long_format = false);
|
||||
|
||||
std::string GetCoverImagePathForEntry(const Entry* entry);
|
||||
std::string GetCoverImagePath(const std::string& path, const std::string& code, const std::string& title);
|
||||
std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial = false);
|
||||
|
|
|
@ -100,8 +100,10 @@ void ImGuiManager::DrawPerformanceOverlay()
|
|||
|
||||
#ifdef PCSX2_CORE
|
||||
const bool paused = (VMManager::GetState() == VMState::Paused);
|
||||
const bool fsui_active = FullscreenUI::HasActiveWindow();
|
||||
#else
|
||||
constexpr bool paused = false;
|
||||
const bool paused = false;
|
||||
const bool fsui_active = false;
|
||||
#endif
|
||||
|
||||
if (!paused)
|
||||
|
@ -221,7 +223,7 @@ void ImGuiManager::DrawPerformanceOverlay()
|
|||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!fsui_active)
|
||||
{
|
||||
if (GSConfig.OsdShowIndicators)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue