GameList: Support caching entries

This commit is contained in:
Connor McLaughlin 2020-01-08 13:37:43 +10:00
parent e0086156ca
commit c03c1451fe
5 changed files with 298 additions and 63 deletions

View file

@ -1,4 +1,8 @@
#include "game_list.h" #include "game_list.h"
#include "YBaseLib/AutoReleasePtr.h"
#include "YBaseLib/BinaryReader.h"
#include "YBaseLib/BinaryWriter.h"
#include "YBaseLib/ByteStream.h"
#include "YBaseLib/FileSystem.h" #include "YBaseLib/FileSystem.h"
#include "YBaseLib/Log.h" #include "YBaseLib/Log.h"
#include "bios.h" #include "bios.h"
@ -8,6 +12,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cctype> #include <cctype>
#include <string_view>
#include <tinyxml2.h> #include <tinyxml2.h>
#include <utility> #include <utility>
Log_SetChannel(GameList); Log_SetChannel(GameList);
@ -201,8 +206,37 @@ bool GameList::IsExeFileName(const char* path)
return (extension && (CASE_COMPARE(extension, ".exe") == 0 || CASE_COMPARE(extension, ".psexe") == 0)); return (extension && (CASE_COMPARE(extension, ".exe") == 0 || CASE_COMPARE(extension, ".psexe") == 0));
} }
static std::string_view GetFileNameFromPath(const char* path)
{
const char* filename_end = path + std::strlen(path);
const char* filename_start = std::max(std::strrchr(path, '/'), std::strrchr(path, '\\'));
if (!filename_start)
return std::string_view(path, filename_end - path);
else
return std::string_view(filename_start + 1, filename_end - filename_start);
}
static std::string_view GetTitleForPath(const char* path)
{
const char* extension = std::strrchr(path, '.');
if (path == extension)
return path;
const char* path_end = path + std::strlen(path);
const char* title_end = extension ? (extension - 1) : (path_end);
const char* title_start = std::max(std::strrchr(path, '/'), std::strrchr(path, '\\'));
if (!title_start || title_start == path)
return std::string_view(path, title_end - title_start);
else
return std::string_view(title_start + 1, title_end - title_start);
}
bool GameList::GetExeListEntry(const char* path, GameListEntry* entry) bool GameList::GetExeListEntry(const char* path, GameListEntry* entry)
{ {
FILESYSTEM_STAT_DATA ffd;
if (!FileSystem::StatFile(path, &ffd))
return false;
std::FILE* fp = std::fopen(path, "rb"); std::FILE* fp = std::fopen(path, "rb");
if (!fp) if (!fp)
return false; return false;
@ -230,33 +264,25 @@ bool GameList::GetExeListEntry(const char* path, GameListEntry* entry)
if (!extension) if (!extension)
return false; return false;
const char* title_start = std::max(std::strrchr(path, '/'), std::strrchr(path, '\\')); entry->code.clear();
if (!title_start) entry->title = GetFileNameFromPath(path);
{
entry->title = path;
entry->code = std::string(path, extension - path - 1);
}
else
{
entry->title = title_start + 1;
entry->code = std::string(title_start + 1, extension - title_start - 1);
}
// no way to detect region... // no way to detect region...
entry->path = path; entry->path = path;
entry->region = ConsoleRegion::NTSC_U; entry->region = ConsoleRegion::NTSC_U;
entry->total_size = ZeroExtend64(file_size); entry->total_size = ZeroExtend64(file_size);
entry->last_modified_time = ffd.ModificationTime.AsUnixTimestamp();
entry->type = EntryType::PSExe; entry->type = EntryType::PSExe;
return true; return true;
} }
bool GameList::GetGameListEntry(const char* path, GameListEntry* entry) bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
{ {
if (IsExeFileName(path)) if (IsExeFileName(path.c_str()))
return GetExeListEntry(path, entry); return GetExeListEntry(path.c_str(), entry);
std::unique_ptr<CDImage> cdi = CDImage::Open(path); std::unique_ptr<CDImage> cdi = CDImage::Open(path.c_str());
if (!cdi) if (!cdi)
return false; return false;
@ -268,6 +294,15 @@ bool GameList::GetGameListEntry(const char* path, GameListEntry* entry)
entry->type = EntryType::Disc; entry->type = EntryType::Disc;
cdi.reset(); cdi.reset();
if (entry->code.empty())
{
// no game code, so use the filename title
entry->title = GetTitleForPath(path.c_str());
}
else
{
LoadDatabase();
auto iter = m_database.find(entry->code); auto iter = m_database.find(entry->code);
if (iter != m_database.end()) if (iter != m_database.end())
{ {
@ -277,12 +312,162 @@ bool GameList::GetGameListEntry(const char* path, GameListEntry* entry)
else else
{ {
Log_WarningPrintf("'%s' not found in database", entry->code.c_str()); Log_WarningPrintf("'%s' not found in database", entry->code.c_str());
entry->title = entry->code; entry->title = GetTitleForPath(path.c_str());
}
}
FILESYSTEM_STAT_DATA ffd;
if (!FileSystem::StatFile(path.c_str(), &ffd))
return false;
entry->last_modified_time = ffd.ModificationTime.AsUnixTimestamp();
return true;
}
bool GameList::GetGameListEntryFromCache(const std::string& path, GameListEntry* entry)
{
auto iter = m_cache_map.find(path);
if (iter == m_cache_map.end())
return false;
*entry = std::move(iter->second);
m_cache_map.erase(iter);
return true;
}
void GameList::LoadCache()
{
if (m_cache_filename.empty())
return;
ByteStream* stream = FileSystem::OpenFile(m_cache_filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED);
if (!stream)
return;
if (!LoadEntriesFromCache(stream))
{
Log_WarningPrintf("Deleting corrupted cache file '%s'", m_cache_filename.c_str());
stream->Release();
m_cache_map.clear();
DeleteCacheFile();
return;
}
stream->Release();
}
bool GameList::LoadEntriesFromCache(ByteStream* stream)
{
BinaryReader reader(stream);
if (reader.ReadUInt32() != GAME_LIST_CACHE_SIGNATURE || reader.ReadUInt32() != GAME_LIST_CACHE_VERSION)
{
Log_WarningPrintf("Game list cache is corrupted");
return false;
}
String path;
TinyString code;
SmallString title;
u64 total_size;
u64 last_modified_time;
u8 region;
u8 type;
while (stream->GetPosition() != stream->GetSize())
{
if (!reader.SafeReadSizePrefixedString(&path) || !reader.SafeReadSizePrefixedString(&code) ||
!reader.SafeReadSizePrefixedString(&title) || !reader.SafeReadUInt64(&total_size) ||
!reader.SafeReadUInt64(&last_modified_time) || !reader.SafeReadUInt8(&region) ||
region >= static_cast<u8>(ConsoleRegion::Count) || !reader.SafeReadUInt8(&type) ||
type > static_cast<u8>(EntryType::PSExe))
{
Log_WarningPrintf("Game list cache entry is corrupted");
return false;
}
GameListEntry ge;
ge.path = path;
ge.code = code;
ge.title = title;
ge.total_size = total_size;
ge.last_modified_time = last_modified_time;
ge.region = static_cast<ConsoleRegion>(region);
ge.type = static_cast<EntryType>(type);
auto iter = m_cache_map.find(ge.path);
if (iter != m_cache_map.end())
iter->second = std::move(ge);
else
m_cache_map.emplace(path, std::move(ge));
} }
return true; return true;
} }
bool GameList::OpenCacheForWriting()
{
if (m_cache_filename.empty())
return false;
Assert(!m_cache_write_stream);
m_cache_write_stream =
FileSystem::OpenFile(m_cache_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_WRITE |
BYTESTREAM_OPEN_APPEND | BYTESTREAM_OPEN_STREAMED);
if (!m_cache_write_stream)
return false;
if (m_cache_write_stream->GetPosition() == 0)
{
// new cache file, write header
BinaryWriter writer(m_cache_write_stream);
if (!writer.SafeWriteUInt32(GAME_LIST_CACHE_SIGNATURE) || !writer.SafeWriteUInt32(GAME_LIST_CACHE_VERSION))
{
Log_ErrorPrintf("Failed to write game list cache header");
m_cache_write_stream->Release();
m_cache_write_stream = nullptr;
FileSystem::DeleteFile(m_cache_filename.c_str());
return false;
}
}
return true;
}
bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
{
BinaryWriter writer(stream);
bool result = writer.SafeWriteSizePrefixedString(entry->path.c_str());
result &= writer.SafeWriteSizePrefixedString(entry->code.c_str());
result &= writer.SafeWriteSizePrefixedString(entry->title.c_str());
result &= writer.SafeWriteUInt64(entry->total_size);
result &= writer.SafeWriteUInt64(entry->last_modified_time);
result &= writer.SafeWriteUInt8(static_cast<u8>(entry->region));
result &= writer.SafeWriteUInt8(static_cast<u8>(entry->type));
return result;
}
void GameList::CloseCacheFileStream()
{
if (!m_cache_write_stream)
return;
m_cache_write_stream->Commit();
m_cache_write_stream->Release();
m_cache_write_stream = nullptr;
}
void GameList::DeleteCacheFile()
{
Assert(!m_cache_write_stream);
if (!FileSystem::FileExists(m_cache_filename.c_str()))
return;
if (FileSystem::DeleteFile(m_cache_filename.c_str()))
Log_InfoPrintf("Deleted game list cache '%s'", m_cache_filename.c_str());
else
Log_WarningPrintf("Failed to delete game list cache '%s'", m_cache_filename.c_str());
}
void GameList::ScanDirectory(const char* path, bool recursive) void GameList::ScanDirectory(const char* path, bool recursive)
{ {
Log_DevPrintf("Scanning %s%s", path, recursive ? " (recursively)" : ""); Log_DevPrintf("Scanning %s%s", path, recursive ? " (recursively)" : "");
@ -293,8 +478,6 @@ void GameList::ScanDirectory(const char* path, bool recursive)
GameListEntry entry; GameListEntry entry;
for (const FILESYSTEM_FIND_DATA& ffd : files) for (const FILESYSTEM_FIND_DATA& ffd : files)
{ {
Log_DebugPrintf("Trying '%s'...", ffd.FileName);
// if this is a .bin, check if we have a .cue. if there is one, skip it // if this is a .bin, check if we have a .cue. if there is one, skip it
const char* extension = std::strrchr(ffd.FileName, '.'); const char* extension = std::strrchr(ffd.FileName, '.');
if (extension && CASE_COMPARE(extension, ".bin") == 0) if (extension && CASE_COMPARE(extension, ".bin") == 0)
@ -313,13 +496,35 @@ void GameList::ScanDirectory(const char* path, bool recursive)
#endif #endif
} }
// try opening the image std::string entry_path(ffd.FileName);
if (GetGameListEntry(ffd.FileName, &entry)) if (std::any_of(m_entries.begin(), m_entries.end(),
[&entry_path](const GameListEntry& other) { return other.path == entry_path; }))
{ {
continue;
}
Log_DebugPrintf("Trying '%s'...", entry_path.c_str());
// try opening the image
if (!GetGameListEntryFromCache(entry_path, &entry) ||
entry.last_modified_time != ffd.ModificationTime.AsUnixTimestamp())
{
if (GetGameListEntry(entry_path, &entry))
{
if (m_cache_write_stream || OpenCacheForWriting())
{
if (!WriteEntryToCache(&entry, m_cache_write_stream))
Log_WarningPrintf("Failed to write entry '%s' to cache", entry.path.c_str());
}
}
else
{
continue;
}
}
m_entries.push_back(std::move(entry)); m_entries.push_back(std::move(entry));
entry = {}; entry = {};
} }
}
} }
class RedumpDatVisitor final : public tinyxml2::XMLVisitor class RedumpDatVisitor final : public tinyxml2::XMLVisitor
@ -413,7 +618,7 @@ void GameList::AddDirectory(std::string path, bool recursive)
m_search_directories.push_back({path, recursive}); m_search_directories.push_back({path, recursive});
} }
void GameList::SetDirectoriesFromSettings(SettingsInterface& si) void GameList::SetPathsFromSettings(SettingsInterface& si)
{ {
m_search_directories.clear(); m_search_directories.clear();
@ -424,41 +629,63 @@ void GameList::SetDirectoriesFromSettings(SettingsInterface& si)
dirs = si.GetStringList("GameList", "RecursivePaths"); dirs = si.GetStringList("GameList", "RecursivePaths");
for (std::string& dir : dirs) for (std::string& dir : dirs)
m_search_directories.push_back({std::move(dir), true}); m_search_directories.push_back({std::move(dir), true});
m_database_filename = si.GetStringValue("GameList", "RedumpDatabasePath");
m_cache_filename = si.GetStringValue("GameList", "CachePath");
} }
void GameList::RescanAllDirectories() void GameList::Refresh(bool invalidate_cache, bool invalidate_database)
{ {
if (invalidate_cache)
DeleteCacheFile();
else
LoadCache();
if (invalidate_database)
ClearDatabase();
m_entries.clear(); m_entries.clear();
for (const DirectoryEntry& de : m_search_directories) for (const DirectoryEntry& de : m_search_directories)
ScanDirectory(de.path.c_str(), de.recursive); ScanDirectory(de.path.c_str(), de.recursive);
// don't need unused cache entries
CloseCacheFileStream();
m_cache_map.clear();
} }
bool GameList::ParseRedumpDatabase(const char* redump_dat_path) void GameList::LoadDatabase()
{ {
if (m_database_load_tried)
return;
m_database_load_tried = true;
if (m_database_filename.empty())
return;
tinyxml2::XMLDocument doc; tinyxml2::XMLDocument doc;
tinyxml2::XMLError error = doc.LoadFile(redump_dat_path); tinyxml2::XMLError error = doc.LoadFile(m_database_filename.c_str());
if (error != tinyxml2::XML_SUCCESS) if (error != tinyxml2::XML_SUCCESS)
{ {
Log_ErrorPrintf("Failed to parse redump dat '%s': %s", redump_dat_path, Log_ErrorPrintf("Failed to parse redump dat '%s': %s", m_database_filename.c_str(),
tinyxml2::XMLDocument::ErrorIDToName(error)); tinyxml2::XMLDocument::ErrorIDToName(error));
return false; return;
} }
const tinyxml2::XMLElement* datafile_elem = doc.FirstChildElement("datafile"); const tinyxml2::XMLElement* datafile_elem = doc.FirstChildElement("datafile");
if (!datafile_elem) if (!datafile_elem)
{ {
Log_ErrorPrintf("Failed to get datafile element in '%s'", redump_dat_path); Log_ErrorPrintf("Failed to get datafile element in '%s'", m_database_filename.c_str());
return false; return;
} }
RedumpDatVisitor visitor(m_database); RedumpDatVisitor visitor(m_database);
datafile_elem->Accept(&visitor); datafile_elem->Accept(&visitor);
Log_InfoPrintf("Loaded %zu entries from Redump.org database '%s'", m_database.size(), redump_dat_path); Log_InfoPrintf("Loaded %zu entries from Redump.org database '%s'", m_database.size(), m_database_filename.c_str());
return true;
} }
void GameList::ClearDatabase() void GameList::ClearDatabase()
{ {
m_database.clear(); m_database.clear();
m_database_load_tried = false;
} }

View file

@ -7,6 +7,7 @@
#include <vector> #include <vector>
class CDImage; class CDImage;
class ByteStream;
class SettingsInterface; class SettingsInterface;
@ -34,11 +35,13 @@ public:
std::string code; std::string code;
std::string title; std::string title;
u64 total_size; u64 total_size;
u64 last_modified_time;
ConsoleRegion region; ConsoleRegion region;
EntryType type; EntryType type;
}; };
using EntryList = std::vector<GameListEntry>; using EntryList = std::vector<GameListEntry>;
using CacheMap = std::unordered_map<std::string, GameListEntry>;
GameList(); GameList();
~GameList(); ~GameList();
@ -56,14 +59,17 @@ public:
const EntryList& GetEntries() const { return m_entries; } const EntryList& GetEntries() const { return m_entries; }
const u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); } const u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); }
void SetPathsFromSettings(SettingsInterface& si);
void AddDirectory(std::string path, bool recursive); void AddDirectory(std::string path, bool recursive);
void SetDirectoriesFromSettings(SettingsInterface& si); void Refresh(bool invalidate_cache, bool invalidate_database);
void RescanAllDirectories();
bool ParseRedumpDatabase(const char* redump_dat_path);
void ClearDatabase();
private: private:
enum : u32
{
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
GAME_LIST_CACHE_VERSION = 2
};
struct DirectoryEntry struct DirectoryEntry
{ {
std::string path; std::string path;
@ -73,11 +79,27 @@ private:
static bool IsExeFileName(const char* path); static bool IsExeFileName(const char* path);
static bool GetExeListEntry(const char* path, GameListEntry* entry); static bool GetExeListEntry(const char* path, GameListEntry* entry);
bool GetGameListEntry(const char* path, GameListEntry* entry); bool GetGameListEntry(const std::string& path, GameListEntry* entry);
bool GetGameListEntryFromCache(const std::string& path, GameListEntry* entry);
void ScanDirectory(const char* path, bool recursive); void ScanDirectory(const char* path, bool recursive);
void LoadCache();
bool LoadEntriesFromCache(ByteStream* stream);
bool OpenCacheForWriting();
bool WriteEntryToCache(const GameListEntry* entry, ByteStream* stream);
void CloseCacheFileStream();
void DeleteCacheFile();
void LoadDatabase();
void ClearDatabase();
DatabaseMap m_database; DatabaseMap m_database;
EntryList m_entries; EntryList m_entries;
CacheMap m_cache_map;
ByteStream* m_cache_write_stream = nullptr;
std::vector<DirectoryEntry> m_search_directories; std::vector<DirectoryEntry> m_search_directories;
std::string m_cache_filename;
std::string m_database_filename;
bool m_database_load_tried = false;
}; };

View file

@ -281,7 +281,7 @@ void GameListSettingsWidget::onBrowseRedumpPathButtonPressed()
m_ui.redumpDatabasePath->setText(filename); m_ui.redumpDatabasePath->setText(filename);
m_host_interface->getQSettings().setValue("GameList/RedumpDatabasePath", filename); m_host_interface->getQSettings().setValue("GameList/RedumpDatabasePath", filename);
m_host_interface->updateGameListDatabase(true); m_host_interface->refreshGameList(true, true);
} }
void GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed() void GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed()

View file

@ -112,27 +112,14 @@ void QtHostInterface::checkSettings()
void QtHostInterface::createGameList() void QtHostInterface::createGameList()
{ {
m_game_list = std::make_unique<GameList>(); m_game_list = std::make_unique<GameList>();
updateGameListDatabase(false); refreshGameList(false, false);
refreshGameList(false);
} }
void QtHostInterface::updateGameListDatabase(bool refresh_list /*= true*/) void QtHostInterface::refreshGameList(bool invalidate_cache /* = false */, bool invalidate_database /* = false */)
{
m_game_list->ClearDatabase();
const QString redump_dat_path = m_qsettings.value("GameList/RedumpDatabasePath").toString();
if (!redump_dat_path.isEmpty())
m_game_list->ParseRedumpDatabase(redump_dat_path.toStdString().c_str());
if (refresh_list)
refreshGameList(true);
}
void QtHostInterface::refreshGameList(bool invalidate_cache /*= false*/)
{ {
QtSettingsInterface si(m_qsettings); QtSettingsInterface si(m_qsettings);
m_game_list->SetDirectoriesFromSettings(si); m_game_list->SetPathsFromSettings(si);
m_game_list->RescanAllDirectories(); m_game_list->Refresh(invalidate_cache, invalidate_database);
emit gameListRefreshed(); emit gameListRefreshed();
} }

View file

@ -41,8 +41,7 @@ public:
const GameList* getGameList() const { return m_game_list.get(); } const GameList* getGameList() const { return m_game_list.get(); }
GameList* getGameList() { return m_game_list.get(); } GameList* getGameList() { return m_game_list.get(); }
void updateGameListDatabase(bool refresh_list = true); void refreshGameList(bool invalidate_cache = false, bool invalidate_database = false);
void refreshGameList(bool invalidate_cache = false);
bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; } bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; }