Add JSON game database to replace dat parsing

This commit is contained in:
Connor McLaughlin
2021-04-17 14:23:47 +10:00
parent b25030b19a
commit ff14e8aede
24 changed files with 595 additions and 398 deletions

View File

@ -16,6 +16,7 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <ctime>
#include <string_view>
#include <tinyxml2.h>
#include <utility>
@ -158,40 +159,50 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
if (!cdi)
return false;
std::string code = System::GetGameCodeForImage(cdi.get(), true);
DiscRegion region = System::GetRegionFromSystemArea(cdi.get());
if (region == DiscRegion::Other)
region = System::GetRegionForCode(code);
entry->path = path;
entry->code = std::move(code);
entry->region = region;
entry->total_size = static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
entry->type = GameListEntryType::Disc;
entry->compatibility_rating = GameListCompatibilityRating::Unknown;
if (entry->code.empty())
// try the database first
LoadDatabase();
GameDatabaseEntry dbentry;
if (!m_database.GetEntryForDisc(cdi.get(), &dbentry))
{
// no game code, so use the filename title
entry->code = System::GetGameCodeForImage(cdi.get(), true);
entry->title = System::GetTitleForPath(path.c_str());
entry->compatibility_rating = GameListCompatibilityRating::Unknown;
entry->release_date = 0;
entry->min_players = 0;
entry->max_players = 0;
entry->min_blocks = 0;
entry->max_blocks = 0;
entry->supported_controllers = ~0u;
}
else
{
const GameListDatabaseEntry* database_entry = GetDatabaseEntryForCode(entry->code);
if (database_entry)
{
entry->title = database_entry->title;
// pull from database
entry->code = std::move(dbentry.serial);
entry->title = std::move(dbentry.title);
entry->genre = std::move(dbentry.genre);
entry->publisher = std::move(dbentry.publisher);
entry->developer = std::move(dbentry.developer);
entry->release_date = dbentry.release_date;
entry->min_players = dbentry.min_players;
entry->max_players = dbentry.max_players;
entry->min_blocks = dbentry.min_blocks;
entry->max_blocks = dbentry.max_blocks;
entry->supported_controllers = dbentry.supported_controllers_mask;
}
if (entry->region != database_entry->region)
Log_WarningPrintf("Region mismatch between disc and database for '%s'", entry->code.c_str());
}
else
{
Log_WarningPrintf("'%s' not found in database", entry->code.c_str());
entry->title = System::GetTitleForPath(path.c_str());
}
// region detection
entry->region = System::GetRegionFromSystemArea(cdi.get());
if (entry->region == DiscRegion::Other)
entry->region = System::GetRegionForCode(entry->code);
if (!entry->code.empty())
{
const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code);
if (compatibility_entry)
entry->compatibility_rating = compatibility_entry->compatibility_rating;
@ -328,30 +339,27 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
while (stream->GetPosition() != stream->GetSize())
{
std::string path;
std::string code;
std::string title;
u64 total_size;
u64 last_modified_time;
u8 region;
GameListEntry ge;
u8 type;
u8 region;
u8 compatibility_rating;
if (!ReadString(stream, &path) || !ReadString(stream, &code) || !ReadString(stream, &title) ||
!ReadU64(stream, &total_size) || !ReadU64(stream, &last_modified_time) || !ReadU8(stream, &region) ||
region >= static_cast<u8>(DiscRegion::Count) || !ReadU8(stream, &type) ||
type >= static_cast<u8>(GameListEntryType::Count) || !ReadU8(stream, &compatibility_rating) ||
if (!ReadU8(stream, &type) || !ReadU8(stream, &region) || !ReadString(stream, &path) ||
!ReadString(stream, &ge.code) || !ReadString(stream, &ge.title) || !ReadString(stream, &ge.genre) ||
!ReadString(stream, &ge.publisher) || !ReadString(stream, &ge.developer) || !ReadU64(stream, &ge.total_size) ||
!ReadU64(stream, &ge.last_modified_time) || !ReadU64(stream, &ge.release_date) ||
!ReadU32(stream, &ge.supported_controllers) || !ReadU8(stream, &ge.min_players) ||
!ReadU8(stream, &ge.max_players) || !ReadU8(stream, &ge.min_blocks) || !ReadU8(stream, &ge.max_blocks) ||
!ReadU8(stream, &compatibility_rating) || region >= static_cast<u8>(DiscRegion::Count) ||
type >= static_cast<u8>(GameListEntryType::Count) ||
compatibility_rating >= static_cast<u8>(GameListCompatibilityRating::Count))
{
Log_WarningPrintf("Game list cache entry is corrupted");
return false;
}
GameListEntry ge;
ge.path = path;
ge.code = std::move(code);
ge.title = std::move(title);
ge.total_size = total_size;
ge.last_modified_time = last_modified_time;
ge.region = static_cast<DiscRegion>(region);
ge.type = static_cast<GameListEntryType>(type);
ge.compatibility_rating = static_cast<GameListCompatibilityRating>(compatibility_rating);
@ -405,13 +413,23 @@ bool GameList::OpenCacheForWriting()
bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
{
bool result = WriteString(stream, entry->path);
bool result = true;
result &= WriteU8(stream, static_cast<u8>(entry->type));
result &= WriteU8(stream, static_cast<u8>(entry->region));
result &= WriteString(stream, entry->path);
result &= WriteString(stream, entry->code);
result &= WriteString(stream, entry->title);
result &= WriteString(stream, entry->genre);
result &= WriteString(stream, entry->publisher);
result &= WriteString(stream, entry->developer);
result &= WriteU64(stream, entry->total_size);
result &= WriteU64(stream, entry->last_modified_time);
result &= WriteU8(stream, static_cast<u8>(entry->region));
result &= WriteU8(stream, static_cast<u8>(entry->type));
result &= WriteU64(stream, entry->release_date);
result &= WriteU32(stream, entry->supported_controllers);
result &= WriteU8(stream, entry->min_players);
result &= WriteU8(stream, entry->max_players);
result &= WriteU8(stream, entry->min_blocks);
result &= WriteU8(stream, entry->max_blocks);
result &= WriteU8(stream, static_cast<u8>(entry->compatibility_rating));
result &= entry->settings.SaveToStream(stream);
return result;
@ -524,84 +542,6 @@ void GameList::ScanDirectory(const char* path, bool recursive, ProgressCallback*
progress->PopState();
}
class GameList::RedumpDatVisitor final : public tinyxml2::XMLVisitor
{
public:
RedumpDatVisitor(DatabaseMap& database) : m_database(database) {}
static std::string FixupSerial(const std::string_view str)
{
std::string ret;
ret.reserve(str.length());
for (size_t i = 0; i < str.length(); i++)
{
if (str[i] == '.' || str[i] == '#')
continue;
else if (str[i] == ',')
break;
else if (str[i] == '_' || str[i] == ' ')
ret.push_back('-');
else
ret.push_back(static_cast<char>(std::toupper(str[i])));
}
return ret;
}
bool VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) override
{
// recurse into gamelist
if (StringUtil::Strcasecmp(element.Name(), "datafile") == 0)
return true;
if (StringUtil::Strcasecmp(element.Name(), "game") != 0)
return false;
const char* name = element.Attribute("name");
if (!name)
return false;
const tinyxml2::XMLElement* serial_elem = element.FirstChildElement("serial");
if (!serial_elem)
return false;
const char* serial_text = serial_elem->GetText();
if (!serial_text)
return false;
// Handle entries like <serial>SCES-00984, SCES-00984#</serial>
const char* start = serial_text;
const char* end = std::strchr(start, ',');
for (;;)
{
std::string code = FixupSerial(end ? std::string_view(start, end - start) : std::string_view(start));
auto iter = m_database.find(code);
if (iter == m_database.end())
{
GameListDatabaseEntry gde;
gde.code = std::move(code);
gde.region = System::GetRegionForCode(gde.code);
gde.title = name;
m_database.emplace(gde.code, std::move(gde));
}
if (!end)
break;
start = end + 1;
while (std::isspace(*start))
start++;
end = std::strchr(start, ',');
}
return false;
}
private:
DatabaseMap& m_database;
};
void GameList::AddDirectory(std::string path, bool recursive)
{
auto iter = std::find_if(m_search_directories.begin(), m_search_directories.end(),
@ -639,15 +579,6 @@ GameListEntry* GameList::GetMutableEntryForPath(const char* path)
return nullptr;
}
const GameListDatabaseEntry* GameList::GetDatabaseEntryForCode(const std::string& code) const
{
if (!m_database_load_tried)
const_cast<GameList*>(this)->LoadDatabase();
auto iter = m_database.find(code);
return (iter != m_database.end()) ? &iter->second : nullptr;
}
const GameListCompatibilityEntry* GameList::GetCompatibilityEntryForCode(const std::string& code) const
{
if (!m_compatibility_list_load_tried)
@ -734,54 +665,12 @@ void GameList::LoadDatabase()
return;
m_database_load_tried = true;
tinyxml2::XMLDocument doc;
if (FileSystem::FileExists(m_user_database_filename.c_str()))
{
std::unique_ptr<ByteStream> stream =
FileSystem::OpenFile(m_user_database_filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED);
if (stream)
{
const std::string xml(FileSystem::ReadStreamToString(stream.get()));
tinyxml2::XMLError error = doc.Parse(xml.data(), xml.size());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to parse redump dat: %s", tinyxml2::XMLDocument::ErrorIDToName(error));
doc.Clear();
}
}
}
if (!doc.RootElement())
{
std::unique_ptr<ByteStream> stream = g_host_interface->OpenPackageFile(
"database" FS_OSPATH_SEPARATOR_STR "redump.dat", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED);
if (stream)
{
const std::string xml(FileSystem::ReadStreamToString(stream.get()));
tinyxml2::XMLError error = doc.Parse(xml.data(), xml.size());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to parse redump dat: %s", tinyxml2::XMLDocument::ErrorIDToName(error));
return;
}
}
}
const tinyxml2::XMLElement* datafile_elem = doc.FirstChildElement("datafile");
if (!datafile_elem)
{
Log_ErrorPrintf("Failed to get datafile element in redump dat");
return;
}
RedumpDatVisitor visitor(m_database);
datafile_elem->Accept(&visitor);
Log_InfoPrintf("Loaded %zu entries from Redump.org database", m_database.size());
m_database.Load();
}
void GameList::ClearDatabase()
{
m_database.clear();
m_database.Unload();
m_database_load_tried = false;
}
@ -1205,3 +1094,20 @@ std::string GameList::GetNewCoverImagePathForEntry(const GameListEntry* entry, c
return g_host_interface->GetUserDirectoryRelativePath("covers" FS_OSPATH_SEPARATOR_STR "%s%s", entry->title.c_str(),
extension);
}
size_t GameListEntry::GetReleaseDateString(char* buffer, size_t buffer_size) const
{
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);
}