2019-11-29 13:46:04 +00:00
|
|
|
#include "game_list.h"
|
2020-01-10 03:31:12 +00:00
|
|
|
#include "common/assert.h"
|
|
|
|
#include "common/byte_stream.h"
|
|
|
|
#include "common/file_system.h"
|
|
|
|
#include "common/log.h"
|
2021-03-11 17:06:10 +00:00
|
|
|
#include "common/make_array.h"
|
2022-07-08 11:57:06 +00:00
|
|
|
#include "common/path.h"
|
2020-03-12 05:32:19 +00:00
|
|
|
#include "common/progress_callback.h"
|
2020-01-10 03:31:12 +00:00
|
|
|
#include "common/string_util.h"
|
2020-09-01 02:29:22 +00:00
|
|
|
#include "core/bios.h"
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "core/host.h"
|
|
|
|
#include "core/host_settings.h"
|
2021-01-24 04:06:52 +00:00
|
|
|
#include "core/psf_loader.h"
|
2020-09-01 02:29:22 +00:00
|
|
|
#include "core/settings.h"
|
|
|
|
#include "core/system.h"
|
2022-07-08 12:43:38 +00:00
|
|
|
#include "util/cd_image.h"
|
2019-11-29 13:46:04 +00:00
|
|
|
#include <algorithm>
|
2019-12-04 11:54:14 +00:00
|
|
|
#include <array>
|
2019-11-29 13:46:04 +00:00
|
|
|
#include <cctype>
|
2021-04-17 04:23:47 +00:00
|
|
|
#include <ctime>
|
2022-07-31 04:46:30 +00:00
|
|
|
#include <unordered_map>
|
2020-01-08 03:37:43 +00:00
|
|
|
#include <string_view>
|
2019-11-30 13:55:05 +00:00
|
|
|
#include <tinyxml2.h>
|
2019-11-29 13:46:04 +00:00
|
|
|
#include <utility>
|
|
|
|
Log_SetChannel(GameList);
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
enum : u32
|
|
|
|
{
|
|
|
|
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
|
|
|
GAME_LIST_CACHE_VERSION = 32
|
|
|
|
};
|
|
|
|
|
|
|
|
namespace GameList {
|
|
|
|
using CacheMap = std::unordered_map<std::string, Entry>;
|
|
|
|
|
|
|
|
static bool GetExeListEntry(const std::string& path, Entry* entry);
|
|
|
|
static bool GetPsfListEntry(const std::string& path, Entry* entry);
|
|
|
|
static bool GetDiscListEntry(const std::string& path, Entry* entry);
|
|
|
|
|
|
|
|
static bool GetGameListEntryFromCache(const std::string& path, 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 std::string GetCacheFilename();
|
|
|
|
static void LoadCache();
|
|
|
|
static bool LoadEntriesFromCache(ByteStream* stream);
|
|
|
|
static bool OpenCacheForWriting();
|
|
|
|
static bool WriteEntryToCache(const Entry* entry);
|
|
|
|
static void CloseCacheFileStream();
|
|
|
|
static void DeleteCacheFile();
|
|
|
|
} // namespace GameList
|
2019-11-29 13:46:04 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
static std::vector<GameList::Entry> m_entries;
|
|
|
|
static std::recursive_mutex s_mutex;
|
|
|
|
static GameList::CacheMap m_cache_map;
|
|
|
|
static std::unique_ptr<ByteStream> m_cache_write_stream;
|
2019-11-29 13:46:04 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
static bool m_game_list_loaded = false;
|
|
|
|
|
|
|
|
const char* GameList::GetEntryTypeName(EntryType type)
|
2019-12-04 11:54:14 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {{"Disc", "PSExe", "Playlist", "PSF"}};
|
2019-12-04 11:54:14 +00:00
|
|
|
return names[static_cast<int>(type)];
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
const char* GameList::GetEntryTypeDisplayName(EntryType type)
|
2020-07-06 14:59:28 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
|
|
|
|
{TRANSLATABLE("GameList", "Disc"), TRANSLATABLE("GameList", "PS-EXE"), TRANSLATABLE("GameList", "Playlist"),
|
|
|
|
TRANSLATABLE("GameList", "PSF")}};
|
|
|
|
return names[static_cast<int>(type)];
|
2020-07-06 14:59:28 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::IsGameListLoaded()
|
2020-05-16 10:01:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return m_game_list_loaded;
|
2020-05-16 10:01:19 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::IsScannableFilename(const std::string_view& path)
|
2021-02-18 08:07:09 +00:00
|
|
|
{
|
|
|
|
// we don't scan bin files because they'll duplicate
|
2022-07-11 13:03:29 +00:00
|
|
|
if (StringUtil::EndsWithNoCase(path, ".bin"))
|
2021-02-18 08:07:09 +00:00
|
|
|
return false;
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
return System::IsLoadableFilename(path);
|
2021-02-18 08:07:09 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry)
|
2019-12-04 11:12:50 +00:00
|
|
|
{
|
2021-04-17 09:13:58 +00:00
|
|
|
std::FILE* fp = FileSystem::OpenCFile(path.c_str(), "rb");
|
2019-12-04 11:12:50 +00:00
|
|
|
if (!fp)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
std::fseek(fp, 0, SEEK_END);
|
|
|
|
const u32 file_size = static_cast<u32>(std::ftell(fp));
|
|
|
|
std::fseek(fp, 0, SEEK_SET);
|
|
|
|
|
|
|
|
BIOS::PSEXEHeader header;
|
|
|
|
if (std::fread(&header, sizeof(header), 1, fp) != 1)
|
|
|
|
{
|
|
|
|
std::fclose(fp);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::fclose(fp);
|
|
|
|
|
|
|
|
if (!BIOS::IsValidPSExeHeader(header, file_size))
|
|
|
|
{
|
2021-04-17 09:13:58 +00:00
|
|
|
Log_DebugPrintf("%s is not a valid PS-EXE", path.c_str());
|
2019-12-04 11:12:50 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-08-20 14:01:35 +00:00
|
|
|
const std::string display_name(FileSystem::GetDisplayNameFromPath(path));
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->serial.clear();
|
2022-08-10 03:03:40 +00:00
|
|
|
entry->title = Path::GetFileTitle(display_name);
|
2021-01-24 04:06:35 +00:00
|
|
|
entry->region = BIOS::GetPSExeDiscRegion(header);
|
2019-12-04 11:12:50 +00:00
|
|
|
entry->total_size = ZeroExtend64(file_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->type = EntryType::PSExe;
|
|
|
|
entry->compatibility = GameDatabase::CompatibilityRating::Unknown;
|
2019-12-04 11:12:50 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::GetPsfListEntry(const std::string& path, Entry* entry)
|
2021-01-24 04:06:52 +00:00
|
|
|
{
|
2021-04-17 09:13:58 +00:00
|
|
|
// we don't need to walk the library chain here - the top file is enough
|
2021-01-24 04:06:52 +00:00
|
|
|
PSFLoader::File file;
|
2021-04-17 09:13:58 +00:00
|
|
|
if (!file.Load(path.c_str()))
|
2021-01-24 04:06:52 +00:00
|
|
|
return false;
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->serial.clear();
|
2021-01-24 04:06:52 +00:00
|
|
|
entry->region = file.GetRegion();
|
2021-04-17 10:20:09 +00:00
|
|
|
entry->total_size = static_cast<u32>(file.GetProgramData().size());
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->type = EntryType::PSF;
|
|
|
|
entry->compatibility = GameDatabase::CompatibilityRating::Unknown;
|
2021-01-24 04:06:52 +00:00
|
|
|
|
|
|
|
// Game - Title
|
|
|
|
std::optional<std::string> game(file.GetTagString("game"));
|
|
|
|
if (game.has_value())
|
|
|
|
{
|
|
|
|
entry->title = std::move(game.value());
|
|
|
|
entry->title += " - ";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
entry->title.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::optional<std::string> title(file.GetTagString("title"));
|
|
|
|
if (title.has_value())
|
2021-08-20 14:01:35 +00:00
|
|
|
{
|
2021-01-24 04:06:52 +00:00
|
|
|
entry->title += title.value();
|
2021-08-20 14:01:35 +00:00
|
|
|
}
|
2021-01-24 04:06:52 +00:00
|
|
|
else
|
2021-08-20 14:01:35 +00:00
|
|
|
{
|
|
|
|
const std::string display_name(FileSystem::GetDisplayNameFromPath(path));
|
2022-08-10 03:03:40 +00:00
|
|
|
entry->title += Path::GetFileTitle(display_name);
|
2021-08-20 14:01:35 +00:00
|
|
|
}
|
2021-01-24 04:06:52 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::GetDiscListEntry(const std::string& path, Entry* entry)
|
2019-11-29 13:46:04 +00:00
|
|
|
{
|
2022-08-27 06:52:24 +00:00
|
|
|
std::unique_ptr<CDImage> cdi = CDImage::Open(path.c_str(), false, nullptr);
|
2019-11-29 13:46:04 +00:00
|
|
|
if (!cdi)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
entry->path = path;
|
2019-11-30 15:27:01 +00:00
|
|
|
entry->total_size = static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->type = EntryType::Disc;
|
|
|
|
entry->compatibility = GameDatabase::CompatibilityRating::Unknown;
|
2019-11-30 13:55:05 +00:00
|
|
|
|
2021-04-17 04:23:47 +00:00
|
|
|
// try the database first
|
2022-07-11 13:03:29 +00:00
|
|
|
const GameDatabase::Entry* dentry = GameDatabase::GetEntryForDisc(cdi.get());
|
|
|
|
if (dentry)
|
|
|
|
{
|
|
|
|
// pull from database
|
|
|
|
entry->serial = dentry->serial;
|
|
|
|
entry->title = dentry->title;
|
|
|
|
entry->genre = dentry->genre;
|
|
|
|
entry->publisher = dentry->publisher;
|
|
|
|
entry->developer = dentry->developer;
|
|
|
|
entry->release_date = dentry->release_date;
|
|
|
|
entry->min_players = dentry->min_players;
|
|
|
|
entry->max_players = dentry->max_players;
|
|
|
|
entry->min_blocks = dentry->min_blocks;
|
|
|
|
entry->max_blocks = dentry->max_blocks;
|
|
|
|
entry->supported_controllers = dentry->supported_controllers;
|
|
|
|
entry->compatibility = dentry->compatibility;
|
|
|
|
}
|
|
|
|
else
|
2019-11-30 13:55:05 +00:00
|
|
|
{
|
2022-08-10 03:03:40 +00:00
|
|
|
const std::string display_name(FileSystem::GetDisplayNameFromPath(path));
|
2022-07-08 11:57:06 +00:00
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
// no game code, so use the filename title
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->serial = System::GetGameCodeForImage(cdi.get(), true);
|
2022-07-08 11:57:06 +00:00
|
|
|
entry->title = Path::GetFileTitle(display_name);
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->compatibility = GameDatabase::CompatibilityRating::Unknown;
|
2021-04-17 04:23:47 +00:00
|
|
|
entry->release_date = 0;
|
|
|
|
entry->min_players = 0;
|
|
|
|
entry->max_players = 0;
|
|
|
|
entry->min_blocks = 0;
|
|
|
|
entry->max_blocks = 0;
|
|
|
|
entry->supported_controllers = ~0u;
|
2019-11-30 13:55:05 +00:00
|
|
|
}
|
2020-03-12 03:51:29 +00:00
|
|
|
|
2021-04-17 04:23:47 +00:00
|
|
|
// region detection
|
|
|
|
entry->region = System::GetRegionFromSystemArea(cdi.get());
|
|
|
|
if (entry->region == DiscRegion::Other)
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->region = System::GetRegionForCode(entry->serial);
|
2019-11-30 13:55:05 +00:00
|
|
|
|
2021-03-26 16:19:23 +00:00
|
|
|
if (cdi->HasSubImages())
|
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
entry->type = EntryType::Playlist;
|
2021-03-26 16:19:23 +00:00
|
|
|
|
|
|
|
std::string image_title(cdi->GetMetadata("title"));
|
|
|
|
if (!image_title.empty())
|
|
|
|
entry->title = std::move(image_title);
|
|
|
|
|
|
|
|
// get the size of all the subimages
|
|
|
|
const u32 subimage_count = cdi->GetSubImageCount();
|
|
|
|
for (u32 i = 1; i < subimage_count; i++)
|
|
|
|
{
|
|
|
|
if (!cdi->SwitchSubImage(i, nullptr))
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to switch to subimage %u in '%s'", i, entry->path.c_str());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
entry->total_size += static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry)
|
|
|
|
{
|
|
|
|
if (System::IsExeFileName(path))
|
|
|
|
return GetExeListEntry(path, entry);
|
|
|
|
if (System::IsPsfFileName(path.c_str()))
|
|
|
|
return GetPsfListEntry(path, entry);
|
|
|
|
return GetDiscListEntry(path, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry)
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
|
|
|
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);
|
2019-11-29 13:46:04 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
bool GameList::LoadEntriesFromCache(ByteStream* stream)
|
|
|
|
{
|
2020-01-10 03:31:12 +00:00
|
|
|
u32 file_signature, file_version;
|
2022-07-11 13:03:29 +00:00
|
|
|
if (!stream->ReadU32(&file_signature) || !stream->ReadU32(&file_version) ||
|
2020-01-10 03:31:12 +00:00
|
|
|
file_signature != GAME_LIST_CACHE_SIGNATURE || file_version != GAME_LIST_CACHE_VERSION)
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
|
|
|
Log_WarningPrintf("Game list cache is corrupted");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (stream->GetPosition() != stream->GetSize())
|
|
|
|
{
|
2020-01-10 03:31:12 +00:00
|
|
|
std::string path;
|
2022-07-11 13:03:29 +00:00
|
|
|
Entry ge;
|
2021-04-17 04:23:47 +00:00
|
|
|
|
2020-01-10 03:31:12 +00:00
|
|
|
u8 type;
|
2021-04-17 04:23:47 +00:00
|
|
|
u8 region;
|
2020-05-16 10:01:19 +00:00
|
|
|
u8 compatibility_rating;
|
2020-01-10 03:31:12 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
if (!stream->ReadU8(&type) || !stream->ReadU8(®ion) || !stream->ReadSizePrefixedString(&path) ||
|
|
|
|
!stream->ReadSizePrefixedString(&ge.serial) || !stream->ReadSizePrefixedString(&ge.title) ||
|
|
|
|
!stream->ReadSizePrefixedString(&ge.genre) || !stream->ReadSizePrefixedString(&ge.publisher) ||
|
|
|
|
!stream->ReadSizePrefixedString(&ge.developer) || !stream->ReadU64(&ge.total_size) ||
|
|
|
|
!stream->ReadU64(reinterpret_cast<u64*>(&ge.last_modified_time)) || !stream->ReadU64(&ge.release_date) ||
|
|
|
|
!stream->ReadU32(&ge.supported_controllers) || !stream->ReadU8(&ge.min_players) ||
|
|
|
|
!stream->ReadU8(&ge.max_players) || !stream->ReadU8(&ge.min_blocks) || !stream->ReadU8(&ge.max_blocks) ||
|
|
|
|
!stream->ReadU8(&compatibility_rating) || region >= static_cast<u8>(DiscRegion::Count) ||
|
|
|
|
type >= static_cast<u8>(EntryType::Count) ||
|
|
|
|
compatibility_rating >= static_cast<u8>(GameDatabase::CompatibilityRating::Count))
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
|
|
|
Log_WarningPrintf("Game list cache entry is corrupted");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-01-22 07:56:58 +00:00
|
|
|
ge.path = path;
|
2020-03-12 03:51:29 +00:00
|
|
|
ge.region = static_cast<DiscRegion>(region);
|
2022-07-11 13:03:29 +00:00
|
|
|
ge.type = static_cast<EntryType>(type);
|
|
|
|
ge.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility_rating);
|
2020-08-20 11:30:11 +00:00
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
auto iter = m_cache_map.find(ge.path);
|
|
|
|
if (iter != m_cache_map.end())
|
|
|
|
iter->second = std::move(ge);
|
|
|
|
else
|
2020-01-22 07:56:58 +00:00
|
|
|
m_cache_map.emplace(std::move(path), std::move(ge));
|
2020-01-08 03:37:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::WriteEntryToCache(const Entry* entry)
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
bool result = true;
|
|
|
|
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->type));
|
|
|
|
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->region));
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->path);
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->serial);
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->title);
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->genre);
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->publisher);
|
|
|
|
result &= m_cache_write_stream->WriteSizePrefixedString(entry->developer);
|
|
|
|
result &= m_cache_write_stream->WriteU64(entry->total_size);
|
|
|
|
result &= m_cache_write_stream->WriteU64(entry->last_modified_time);
|
|
|
|
result &= m_cache_write_stream->WriteU64(entry->release_date);
|
|
|
|
result &= m_cache_write_stream->WriteU32(entry->supported_controllers);
|
|
|
|
result &= m_cache_write_stream->WriteU8(entry->min_players);
|
|
|
|
result &= m_cache_write_stream->WriteU8(entry->max_players);
|
|
|
|
result &= m_cache_write_stream->WriteU8(entry->min_blocks);
|
|
|
|
result &= m_cache_write_stream->WriteU8(entry->max_blocks);
|
|
|
|
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->compatibility));
|
|
|
|
return result;
|
|
|
|
}
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
static std::string GameList::GetCacheFilename()
|
|
|
|
{
|
|
|
|
return Path::Combine(EmuFolders::Cache, "gamelist.cache");
|
|
|
|
}
|
|
|
|
|
|
|
|
void GameList::LoadCache()
|
|
|
|
{
|
|
|
|
std::string filename(GetCacheFilename());
|
|
|
|
std::unique_ptr<ByteStream> stream =
|
|
|
|
ByteStream::OpenFile(filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED);
|
|
|
|
if (!stream)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!LoadEntriesFromCache(stream.get()))
|
2020-09-09 14:23:24 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
Log_WarningPrintf("Deleting corrupted cache file '%s'", filename.c_str());
|
|
|
|
stream.reset();
|
|
|
|
m_cache_map.clear();
|
|
|
|
DeleteCacheFile();
|
|
|
|
return;
|
2020-09-09 14:23:24 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
bool GameList::OpenCacheForWriting()
|
|
|
|
{
|
|
|
|
const std::string cache_filename(GetCacheFilename());
|
|
|
|
Assert(!m_cache_write_stream);
|
|
|
|
|
|
|
|
m_cache_write_stream = ByteStream::OpenFile(cache_filename.c_str(),
|
|
|
|
BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_SEEKABLE);
|
|
|
|
if (m_cache_write_stream)
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
// check the header
|
|
|
|
u32 signature, version;
|
|
|
|
if (m_cache_write_stream->ReadU32(&signature) && signature == GAME_LIST_CACHE_SIGNATURE &&
|
|
|
|
m_cache_write_stream->ReadU32(&version) && version == GAME_LIST_CACHE_VERSION &&
|
|
|
|
m_cache_write_stream->SeekToEnd())
|
2020-01-08 03:37:43 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return true;
|
2020-01-08 03:37:43 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
m_cache_write_stream.reset();
|
|
|
|
}
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
Log_InfoPrintf("Creating new game list cache file: '%s'", cache_filename.c_str());
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
m_cache_write_stream = ByteStream::OpenFile(
|
|
|
|
cache_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE);
|
2020-04-25 05:23:36 +00:00
|
|
|
if (!m_cache_write_stream)
|
2022-07-11 13:03:29 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
// new cache file, write header
|
|
|
|
if (!m_cache_write_stream->WriteU32(GAME_LIST_CACHE_SIGNATURE) ||
|
|
|
|
!m_cache_write_stream->WriteU32(GAME_LIST_CACHE_VERSION))
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to write game list cache header");
|
|
|
|
m_cache_write_stream.reset();
|
|
|
|
FileSystem::DeleteFile(cache_filename.c_str());
|
|
|
|
return false;
|
|
|
|
}
|
2020-04-25 05:23:36 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
return true;
|
2020-04-25 05:23:36 +00:00
|
|
|
}
|
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
void GameList::CloseCacheFileStream()
|
|
|
|
{
|
|
|
|
if (!m_cache_write_stream)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_cache_write_stream->Commit();
|
2020-01-10 03:31:12 +00:00
|
|
|
m_cache_write_stream.reset();
|
2020-01-08 03:37:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void GameList::DeleteCacheFile()
|
|
|
|
{
|
|
|
|
Assert(!m_cache_write_stream);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
const std::string filename(GetCacheFilename());
|
|
|
|
if (!FileSystem::FileExists(filename.c_str()))
|
2020-01-08 03:37:43 +00:00
|
|
|
return;
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
if (FileSystem::DeleteFile(filename.c_str()))
|
|
|
|
Log_InfoPrintf("Deleted game list cache '%s'", filename.c_str());
|
2020-01-08 03:37:43 +00:00
|
|
|
else
|
2022-07-11 13:03:29 +00:00
|
|
|
Log_WarningPrintf("Failed to delete game list cache '%s'", filename.c_str());
|
2020-01-08 03:37:43 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const std::string& path)
|
2019-11-29 13:46:04 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
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)
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Scanning %s%s", path, recursive ? " (recursively)" : "");
|
2019-11-29 13:46:04 +00:00
|
|
|
|
2020-03-12 05:32:19 +00:00
|
|
|
progress->SetFormattedStatusText("Scanning directory '%s'%s...", path, recursive ? " (recursively)" : "");
|
|
|
|
|
2019-11-29 13:46:04 +00:00
|
|
|
FileSystem::FindResultsArray files;
|
2021-07-21 10:25:52 +00:00
|
|
|
FileSystem::FindFiles(path, "*",
|
|
|
|
recursive ? (FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RECURSIVE) :
|
|
|
|
(FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES),
|
|
|
|
&files);
|
2022-07-22 13:33:59 +00:00
|
|
|
if (files.empty())
|
|
|
|
return;
|
2019-11-29 13:46:04 +00:00
|
|
|
|
2022-07-22 13:33:59 +00:00
|
|
|
progress->PushState();
|
2020-03-12 05:32:19 +00:00
|
|
|
progress->SetProgressRange(static_cast<u32>(files.size()));
|
|
|
|
progress->SetProgressValue(0);
|
|
|
|
|
2022-07-22 13:33:59 +00:00
|
|
|
u32 files_scanned = 0;
|
2021-04-17 09:13:58 +00:00
|
|
|
for (FILESYSTEM_FIND_DATA& ffd : files)
|
2019-11-29 13:46:04 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
files_scanned++;
|
2019-11-29 13:46:04 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
if (progress->IsCancelled() || !GameList::IsScannableFilename(ffd.FileName) ||
|
|
|
|
IsPathExcluded(excluded_paths, ffd.FileName))
|
|
|
|
{
|
2020-01-08 03:37:43 +00:00
|
|
|
continue;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
std::unique_lock lock(s_mutex);
|
|
|
|
if (GetEntryForPath(ffd.FileName.c_str()) || AddFileFromCache(ffd.FileName, ffd.ModificationTime) || only_cache)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2020-03-12 05:32:19 +00:00
|
|
|
|
2021-04-17 09:13:58 +00:00
|
|
|
// ownership of fp is transferred
|
2021-04-24 06:03:28 +00:00
|
|
|
progress->SetFormattedStatusText("Scanning '%s'...", FileSystem::GetDisplayNameFromPath(ffd.FileName).c_str());
|
2022-07-08 11:57:06 +00:00
|
|
|
ScanFile(std::move(ffd.FileName), ffd.ModificationTime);
|
2022-07-11 13:03:29 +00:00
|
|
|
progress->SetProgressValue(files_scanned);
|
2019-11-29 13:46:04 +00:00
|
|
|
}
|
2020-03-12 05:32:19 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
progress->SetProgressValue(files_scanned);
|
2020-03-12 05:32:19 +00:00
|
|
|
progress->PopState();
|
2019-11-29 13:46:04 +00:00
|
|
|
}
|
2019-11-30 13:55:05 +00:00
|
|
|
|
2022-07-08 11:57:06 +00:00
|
|
|
bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp)
|
2021-04-17 09:13:58 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
if (std::any_of(m_entries.begin(), m_entries.end(), [&path](const Entry& other) { return other.path == path; }))
|
2021-04-17 09:13:58 +00:00
|
|
|
{
|
|
|
|
// already exists
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
Entry entry;
|
|
|
|
if (!GetGameListEntryFromCache(path, &entry) || entry.last_modified_time != timestamp)
|
2021-04-17 09:13:58 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
m_entries.push_back(std::move(entry));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-08 11:57:06 +00:00
|
|
|
bool GameList::ScanFile(std::string path, std::time_t timestamp)
|
2021-04-17 09:13:58 +00:00
|
|
|
{
|
|
|
|
Log_DevPrintf("Scanning '%s'...", path.c_str());
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
Entry entry;
|
|
|
|
if (!PopulateEntryFromPath(path, &entry))
|
2021-04-17 09:13:58 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
entry.path = std::move(path);
|
2022-07-11 13:03:29 +00:00
|
|
|
entry.last_modified_time = timestamp;
|
2021-04-17 09:13:58 +00:00
|
|
|
|
|
|
|
if (m_cache_write_stream || OpenCacheForWriting())
|
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
if (!WriteEntryToCache(&entry))
|
2021-04-17 09:13:58 +00:00
|
|
|
Log_WarningPrintf("Failed to write entry '%s' to cache", entry.path.c_str());
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
std::unique_lock lock(s_mutex);
|
2021-04-17 09:13:58 +00:00
|
|
|
m_entries.push_back(std::move(entry));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
std::unique_lock<std::recursive_mutex> GameList::GetLock()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return std::unique_lock<std::recursive_mutex>(s_mutex);
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
const GameList::Entry* GameList::GetEntryByIndex(u32 index)
|
|
|
|
{
|
|
|
|
return (index < m_entries.size()) ? &m_entries[index] : nullptr;
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
const GameList::Entry* GameList::GetEntryForPath(const char* path)
|
2020-01-24 04:50:44 +00:00
|
|
|
{
|
|
|
|
const size_t path_length = std::strlen(path);
|
2022-07-11 13:03:29 +00:00
|
|
|
for (const Entry& entry : m_entries)
|
2020-01-24 04:50:44 +00:00
|
|
|
{
|
2020-01-24 04:50:46 +00:00
|
|
|
if (entry.path.size() == path_length && StringUtil::Strcasecmp(entry.path.c_str(), path) == 0)
|
2020-01-24 04:50:44 +00:00
|
|
|
return &entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
const GameList::Entry* GameList::GetEntryBySerial(const std::string_view& serial)
|
2020-08-20 11:30:11 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
for (const Entry& entry : m_entries)
|
2020-08-20 11:30:11 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
if (entry.serial.length() == serial.length() &&
|
|
|
|
StringUtil::Strncasecmp(entry.serial.c_str(), serial.data(), serial.length()) == 0)
|
|
|
|
{
|
2020-08-20 11:30:11 +00:00
|
|
|
return &entry;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2020-08-20 11:30:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
u32 GameList::GetEntryCount()
|
2020-05-16 10:01:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return static_cast<u32>(m_entries.size());
|
2020-05-16 10:01:19 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* progress /* = nullptr */)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2021-07-27 08:11:32 +00:00
|
|
|
m_game_list_loaded = true;
|
|
|
|
|
2020-03-12 05:32:19 +00:00
|
|
|
if (!progress)
|
|
|
|
progress = ProgressCallback::NullProgressCallback;
|
|
|
|
|
2020-01-08 03:37:43 +00:00
|
|
|
if (invalidate_cache)
|
|
|
|
DeleteCacheFile();
|
|
|
|
else
|
|
|
|
LoadCache();
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// don't delete the old entries, since the frontend might still access them
|
|
|
|
std::vector<Entry> old_entries;
|
2020-03-12 05:32:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
std::unique_lock lock(s_mutex);
|
|
|
|
old_entries.swap(m_entries);
|
2020-03-12 05:32:19 +00:00
|
|
|
}
|
2020-01-08 03:37:43 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
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"));
|
2020-05-16 10:01:19 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
if (!dirs.empty() || !recursive_dirs.empty())
|
2020-05-16 10:01:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
progress->SetProgressRange(static_cast<u32>(dirs.size() + recursive_dirs.size()));
|
|
|
|
progress->SetProgressValue(0);
|
2020-05-16 10:01:19 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// we manually count it here, because otherwise pop state updates it itself
|
|
|
|
int directory_counter = 0;
|
|
|
|
for (const std::string& dir : dirs)
|
2020-05-16 10:01:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
if (progress->IsCancelled())
|
2020-05-16 10:01:19 +00:00
|
|
|
break;
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, progress);
|
|
|
|
progress->SetProgressValue(++directory_counter);
|
2020-11-27 07:58:06 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
for (const std::string& dir : recursive_dirs)
|
2020-05-16 10:01:19 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
if (progress->IsCancelled())
|
|
|
|
break;
|
2020-08-20 11:30:11 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, progress);
|
|
|
|
progress->SetProgressValue(++directory_counter);
|
2020-11-27 07:58:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// don't need unused cache entries
|
|
|
|
CloseCacheFileStream();
|
|
|
|
m_cache_map.clear();
|
2021-05-12 17:22:28 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
std::string GameList::GetCoverImagePathForEntry(const Entry* entry)
|
2020-08-20 11:30:11 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return GetCoverImagePath(entry->path, entry->serial, entry->title);
|
2020-08-20 11:30:11 +00:00
|
|
|
}
|
2020-09-23 13:43:32 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
static std::string GetFullCoverPath(const std::string_view& filename, const std::string_view& extension)
|
2021-05-02 04:57:52 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
return fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.{}", EmuFolders::Covers, filename, extension);
|
2021-05-02 04:57:52 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
std::string GameList::GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title)
|
2020-09-23 13:43:32 +00:00
|
|
|
{
|
2021-03-11 17:06:10 +00:00
|
|
|
static constexpr auto extensions = make_array("jpg", "jpeg", "png", "webp");
|
2020-09-23 13:43:32 +00:00
|
|
|
|
|
|
|
for (const char* extension : extensions)
|
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
// Prioritize lookup by serial (Most specific)
|
|
|
|
if (!serial.empty())
|
2020-12-12 02:23:53 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
const std::string cover_path(GetFullCoverPath(serial, extension));
|
|
|
|
if (FileSystem::FileExists(cover_path.c_str()))
|
|
|
|
return cover_path;
|
2020-12-12 02:23:53 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// Try file title (for modded games or specific like above)
|
|
|
|
const std::string_view file_title(Path::GetFileTitle(path));
|
|
|
|
if (!file_title.empty() && title != file_title)
|
2020-09-24 02:22:29 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
const std::string cover_path(GetFullCoverPath(file_title, extension));
|
|
|
|
if (FileSystem::FileExists(cover_path.c_str()))
|
|
|
|
return cover_path;
|
2020-09-24 02:22:29 +00:00
|
|
|
}
|
2020-09-23 13:43:32 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// Last resort, check the game title
|
|
|
|
if (!title.empty())
|
2020-09-24 02:22:29 +00:00
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
const std::string cover_path(GetFullCoverPath(title, extension));
|
|
|
|
if (FileSystem::FileExists(cover_path.c_str()))
|
|
|
|
return cover_path;
|
2020-09-24 02:22:29 +00:00
|
|
|
}
|
2020-09-23 13:43:32 +00:00
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
return {};
|
2020-09-23 13:43:32 +00:00
|
|
|
}
|
2020-10-03 13:20:48 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename)
|
2020-10-03 13:20:48 +00:00
|
|
|
{
|
|
|
|
const char* extension = std::strrchr(new_filename, '.');
|
|
|
|
if (!extension)
|
|
|
|
return {};
|
|
|
|
|
|
|
|
std::string existing_filename = GetCoverImagePathForEntry(entry);
|
|
|
|
if (!existing_filename.empty())
|
|
|
|
{
|
|
|
|
std::string::size_type pos = existing_filename.rfind('.');
|
|
|
|
if (pos != std::string::npos && existing_filename.compare(pos, std::strlen(extension), extension) == 0)
|
|
|
|
return existing_filename;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
// Check for illegal characters, use serial instead.
|
2022-07-30 11:46:14 +00:00
|
|
|
const std::string sanitized_name(Path::SanitizeFileName(entry->title));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
std::string name;
|
|
|
|
if (sanitized_name != entry->title)
|
|
|
|
name = fmt::format("{}{}", entry->serial, extension);
|
|
|
|
else
|
|
|
|
name = fmt::format("{}{}", entry->title, extension);
|
|
|
|
|
2022-07-29 14:47:49 +00:00
|
|
|
return Path::Combine(EmuFolders::Covers, name);
|
2020-10-03 13:20:48 +00:00
|
|
|
}
|
2021-04-17 04:23:47 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
size_t GameList::Entry::GetReleaseDateString(char* buffer, size_t buffer_size) const
|
2021-04-17 04:23:47 +00:00
|
|
|
{
|
|
|
|
if (release_date == 0)
|
|
|
|
return StringUtil::Strlcpy(buffer, "Unknown", buffer_size);
|
|
|
|
|
|
|
|
std::time_t date_as_time = static_cast<std::time_t>(release_date);
|
|
|
|
#ifdef _WIN32
|
|
|
|
tm date_tm = {};
|
|
|
|
gmtime_s(&date_tm, &date_as_time);
|
|
|
|
#else
|
|
|
|
tm date_tm = {};
|
|
|
|
gmtime_r(&date_as_time, &date_tm);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
return std::strftime(buffer, buffer_size, "%d %B %Y", &date_tm);
|
|
|
|
}
|