GameList: Add support for compatibility database

This commit is contained in:
Connor McLaughlin 2020-05-16 20:01:19 +10:00
parent 6ebd9236c5
commit d781de2ce6
5 changed files with 397 additions and 21 deletions

View file

@ -207,6 +207,15 @@ bool GameList::IsPsfFileName(const char* path)
return (extension && StringUtil::Strcasecmp(extension, ".psf") == 0);
}
const char* GameList::GetGameListCompatibilityRatingString(GameListCompatibilityRating rating)
{
static constexpr std::array<const char*, static_cast<size_t>(GameListCompatibilityRating::Count)> names = {
{"Unknown", "Doesn't Boot", "Crashes In Intro", "Crashes In-Game", "Graphical/Audio Issues", "No Issues"}};
return (rating >= GameListCompatibilityRating::Unknown && rating < GameListCompatibilityRating::Count) ?
names[static_cast<int>(rating)] :
"";
}
static std::string_view GetFileNameFromPath(const char* path)
{
const char* filename_end = path + std::strlen(path);
@ -303,6 +312,7 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
{
// no game code, so use the filename title
entry->title = GetTitleForPath(path.c_str());
entry->compatibility_rating = GameListCompatibilityRating::Unknown;
}
else
{
@ -319,6 +329,16 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
Log_WarningPrintf("'%s' not found in database", entry->code.c_str());
entry->title = GetTitleForPath(path.c_str());
}
const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code);
if (compatibility_entry)
{
entry->compatibility_rating = compatibility_entry->compatibility_rating;
}
else
{
Log_WarningPrintf("'%s' (%s) not found in compatibility list", entry->code.c_str(), entry->title.c_str());
}
}
FILESYSTEM_STAT_DATA ffd;
@ -428,11 +448,13 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
u64 last_modified_time;
u8 region;
u8 type;
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::PSExe))
type > static_cast<u8>(GameListEntryType::PSExe) || !ReadU8(stream, &compatibility_rating) ||
compatibility_rating >= static_cast<u8>(GameListCompatibilityRating::Count))
{
Log_WarningPrintf("Game list cache entry is corrupted");
return false;
@ -446,6 +468,7 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
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);
auto iter = m_cache_map.find(ge.path);
if (iter != m_cache_map.end())
@ -494,6 +517,7 @@ bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
result &= WriteU64(stream, entry->last_modified_time);
result &= WriteU8(stream, static_cast<u8>(entry->region));
result &= WriteU8(stream, static_cast<u8>(entry->type));
result &= WriteU8(stream, static_cast<u8>(entry->compatibility_rating));
return result;
}
@ -514,6 +538,23 @@ void GameList::CloseCacheFileStream()
m_cache_write_stream.reset();
}
void GameList::RewriteCacheFile()
{
CloseCacheFileStream();
DeleteCacheFile();
if (OpenCacheForWriting())
{
for (const auto& it : m_entries)
{
if (!WriteEntryToCache(&it, m_cache_write_stream.get()))
{
Log_ErrorPrintf("Failed to write '%s' to new cache file", it.title.c_str());
break;
}
}
}
}
void GameList::DeleteCacheFile()
{
Assert(!m_cache_write_stream);
@ -714,6 +755,15 @@ const GameListDatabaseEntry* GameList::GetDatabaseEntryForCode(const std::string
return (iter != m_database.end()) ? &iter->second : nullptr;
}
const GameListCompatibilityEntry* GameList::GetCompatibilityEntryForCode(const std::string& code) const
{
if (!m_compatibility_list_load_tried)
const_cast<GameList*>(this)->LoadCompatibilityList();
auto iter = m_compatibility_list.find(code);
return (iter != m_compatibility_list.end()) ? &iter->second : nullptr;
}
void GameList::SetSearchDirectoriesFromSettings(SettingsInterface& si)
{
m_search_directories.clear();
@ -764,6 +814,31 @@ void GameList::Refresh(bool invalidate_cache, bool invalidate_database, Progress
m_cache_map.clear();
}
void GameList::UpdateCompatibilityEntry(GameListCompatibilityEntry new_entry, bool save_to_list /*= true*/)
{
auto iter = m_compatibility_list.find(new_entry.code.c_str());
if (iter != m_compatibility_list.end())
{
iter->second = std::move(new_entry);
}
else
{
std::string key(new_entry.code);
iter = m_compatibility_list.emplace(std::move(key), std::move(new_entry)).first;
}
auto game_list_it = std::find_if(m_entries.begin(), m_entries.end(),
[&iter](const GameListEntry& ge) { return (ge.code == iter->second.code); });
if (game_list_it != m_entries.end() && game_list_it->compatibility_rating != iter->second.compatibility_rating)
{
game_list_it->compatibility_rating = iter->second.compatibility_rating;
RewriteCacheFile();
}
if (save_to_list)
SaveCompatibilityDatabaseForEntry(&iter->second);
}
void GameList::LoadDatabase()
{
if (m_database_load_tried)
@ -799,3 +874,278 @@ void GameList::ClearDatabase()
m_database.clear();
m_database_load_tried = false;
}
class GameList::CompatibilityListVisitor final : public tinyxml2::XMLVisitor
{
public:
CompatibilityListVisitor(CompatibilityMap& 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(), "compatibility-list") == 0)
return true;
if (StringUtil::Strcasecmp(element.Name(), "entry") != 0)
return false;
const char* attr = element.Attribute("code");
std::string code(attr ? attr : "");
attr = element.Attribute("title");
std::string title(attr ? attr : "");
attr = element.Attribute("region");
std::optional<DiscRegion> region = Settings::ParseDiscRegionName(attr ? attr : "");
const int compatibility = element.IntAttribute("compatibility");
const tinyxml2::XMLElement* upscaling_elem = element.FirstChildElement("upscaling-issues");
const tinyxml2::XMLElement* version_tested_elm = element.FirstChildElement("version-tested");
const tinyxml2::XMLElement* comments_elem = element.FirstChildElement("comments");
const char* upscaling = upscaling_elem ? upscaling_elem->GetText() : nullptr;
const char* version_tested = version_tested_elm ? version_tested_elm->GetText() : nullptr;
const char* comments = comments_elem ? comments_elem->GetText() : nullptr;
if (code.empty() || !region.has_value() || compatibility < 0 ||
compatibility >= static_cast<int>(GameListCompatibilityRating::Count))
{
Log_ErrorPrintf("Missing child node at line %d", element.GetLineNum());
return false;
}
auto iter = m_database.find(code);
if (iter != m_database.end())
{
Log_ErrorPrintf("Duplicate game code in compatibility list: '%s'", code.c_str());
return false;
}
GameListCompatibilityEntry entry;
entry.code = code;
entry.title = title;
entry.region = region.value();
entry.compatibility_rating = static_cast<GameListCompatibilityRating>(compatibility);
if (upscaling)
entry.upscaling_issues = upscaling;
if (version_tested)
entry.version_tested = version_tested;
if (comments)
entry.comments = comments;
m_database.emplace(std::move(code), std::move(entry));
return false;
}
private:
CompatibilityMap& m_database;
};
void GameList::LoadCompatibilityList()
{
if (m_compatibility_list_load_tried)
return;
m_compatibility_list_load_tried = true;
if (m_compatibility_list_filename.empty())
return;
tinyxml2::XMLDocument doc;
tinyxml2::XMLError error = doc.LoadFile(m_compatibility_list_filename.c_str());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to parse compatibility list '%s': %s", m_compatibility_list_filename.c_str(),
tinyxml2::XMLDocument::ErrorIDToName(error));
return;
}
const tinyxml2::XMLElement* datafile_elem = doc.FirstChildElement("compatibility-list");
if (!datafile_elem)
{
Log_ErrorPrintf("Failed to get compatibility-list element in '%s'", m_compatibility_list_filename.c_str());
return;
}
CompatibilityListVisitor visitor(m_compatibility_list);
datafile_elem->Accept(&visitor);
Log_InfoPrintf("Loaded %zu entries from compatibility list '%s'", m_compatibility_list.size(),
m_compatibility_list_filename.c_str());
}
static void InitElementForCompatibilityEntry(tinyxml2::XMLDocument* doc, tinyxml2::XMLElement* entry_elem,
const GameListCompatibilityEntry* entry)
{
entry_elem->SetAttribute("code", entry->code.c_str());
entry_elem->SetAttribute("title", entry->title.c_str());
entry_elem->SetAttribute("region", Settings::GetDiscRegionName(entry->region));
entry_elem->SetAttribute("compatibility", static_cast<int>(entry->compatibility_rating));
tinyxml2::XMLElement* elem = entry_elem->FirstChildElement("compatibility");
if (!elem)
{
elem = doc->NewElement("compatibility");
entry_elem->InsertEndChild(elem);
}
elem->SetText(GameList::GetGameListCompatibilityRatingString(entry->compatibility_rating));
if (!entry->upscaling_issues.empty())
{
elem = entry_elem->FirstChildElement("upscaling-issues");
if (!entry->upscaling_issues.empty())
{
if (!elem)
{
elem = doc->NewElement("upscaling-issues");
entry_elem->InsertEndChild(elem);
}
elem->SetText(entry->upscaling_issues.c_str());
}
else
{
if (elem)
entry_elem->DeleteChild(elem);
}
}
if (!entry->version_tested.empty())
{
elem = entry_elem->FirstChildElement("version-tested");
if (!entry->version_tested.empty())
{
if (!elem)
{
elem = doc->NewElement("version-tested");
entry_elem->InsertEndChild(elem);
}
elem->SetText(entry->version_tested.c_str());
}
else
{
if (elem)
entry_elem->DeleteChild(elem);
}
}
if (!entry->comments.empty())
{
elem = entry_elem->FirstChildElement("comments");
if (!entry->comments.empty())
{
if (!elem)
{
elem = doc->NewElement("comments");
entry_elem->InsertEndChild(elem);
}
elem->SetText(entry->comments.c_str());
}
else
{
if (elem)
entry_elem->DeleteChild(elem);
}
}
}
bool GameList::SaveCompatibilityDatabase()
{
if (m_compatibility_list_filename.empty())
return false;
tinyxml2::XMLDocument doc;
tinyxml2::XMLElement* root_elem = doc.NewElement("compatibility-list");
doc.InsertEndChild(root_elem);
for (const auto& it : m_compatibility_list)
{
const GameListCompatibilityEntry* entry = &it.second;
tinyxml2::XMLElement* entry_elem = doc.NewElement("entry");
root_elem->InsertEndChild(entry_elem);
InitElementForCompatibilityEntry(&doc, entry_elem, entry);
}
tinyxml2::XMLError error = doc.SaveFile(m_compatibility_list_filename.c_str());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to save compatibility list '%s': %s", m_compatibility_list_filename.c_str(),
tinyxml2::XMLDocument::ErrorIDToName(error));
return false;
}
Log_InfoPrintf("Saved %zu entries to compatibility list '%s'", m_compatibility_list.size(),
m_compatibility_list_filename.c_str());
return true;
}
bool GameList::SaveCompatibilityDatabaseForEntry(const GameListCompatibilityEntry* entry)
{
if (m_compatibility_list_filename.empty())
return false;
if (!FileSystem::FileExists(m_compatibility_list_filename.c_str()))
return SaveCompatibilityDatabase();
tinyxml2::XMLDocument doc;
tinyxml2::XMLError error = doc.LoadFile(m_compatibility_list_filename.c_str());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to parse compatibility list '%s': %s", m_compatibility_list_filename.c_str(),
tinyxml2::XMLDocument::ErrorIDToName(error));
return false;
}
tinyxml2::XMLElement* root_elem = doc.FirstChildElement("compatibility-list");
if (!root_elem)
{
Log_ErrorPrintf("Failed to get compatibility-list element in '%s'", m_compatibility_list_filename.c_str());
return false;
}
tinyxml2::XMLElement* current_entry_elem = root_elem->FirstChildElement();
while (current_entry_elem)
{
const char* existing_code = current_entry_elem->Attribute("code");
if (existing_code && StringUtil::Strcasecmp(entry->code.c_str(), existing_code) == 0)
{
// update the existing element
InitElementForCompatibilityEntry(&doc, current_entry_elem, entry);
break;
}
current_entry_elem = current_entry_elem->NextSiblingElement();
}
if (!current_entry_elem)
{
// not found, insert
tinyxml2::XMLElement* entry_elem = doc.NewElement("entry");
root_elem->InsertEndChild(entry_elem);
InitElementForCompatibilityEntry(&doc, entry_elem, entry);
}
error = doc.SaveFile(m_compatibility_list_filename.c_str());
if (error != tinyxml2::XML_SUCCESS)
{
Log_ErrorPrintf("Failed to update compatibility list '%s': %s", m_compatibility_list_filename.c_str(),
tinyxml2::XMLDocument::ErrorIDToName(error));
return false;
}
Log_InfoPrintf("Updated compatibility list '%s'", m_compatibility_list_filename.c_str());
return true;
}

View file

@ -19,6 +19,17 @@ enum class GameListEntryType
PSExe
};
enum class GameListCompatibilityRating
{
Unknown = 0,
DoesntBoot = 1,
CrashesInIntro = 2,
CrashesInGame = 3,
GraphicalAudioIssues = 4,
NoIssues = 5,
Count,
};
struct GameListDatabaseEntry
{
std::string code;
@ -35,6 +46,18 @@ struct GameListEntry
u64 last_modified_time;
DiscRegion region;
GameListEntryType type;
GameListCompatibilityRating compatibility_rating;
};
struct GameListCompatibilityEntry
{
std::string code;
std::string title;
std::string version_tested;
std::string upscaling_issues;
std::string comments;
DiscRegion region;
GameListCompatibilityRating compatibility_rating;
};
class GameList
@ -53,6 +76,9 @@ public:
/// Returns true if the filename is a Portable Sound Format file we can uncompress/load.
static bool IsPsfFileName(const char* path);
/// Returns a string representation of a compatibility level.
static const char* GetGameListCompatibilityRatingString(GameListCompatibilityRating rating);
static std::string GetGameCodeForImage(CDImage* cdi);
static std::string GetGameCodeForPath(const char* image_path);
static DiscRegion GetRegionForCode(std::string_view code);
@ -66,12 +92,15 @@ public:
const GameListEntry* GetEntryForPath(const char* path) const;
const GameListDatabaseEntry* GetDatabaseEntryForCode(const std::string& code) const;
const GameListCompatibilityEntry* GetCompatibilityEntryForCode(const std::string& code) const;
const std::string& GetCacheFilename() const { return m_cache_filename; }
const std::string& GetDatabaseFilename() const { return m_database_filename; }
const std::string& GetCompatibilityFilename() const { return m_database_filename; }
void SetCacheFilename(std::string filename) { m_cache_filename = std::move(filename); }
void SetDatabaseFilename(std::string filename) { m_database_filename = std::move(filename); }
void SetCompatibilityFilename(std::string filename) { m_compatibility_list_filename = std::move(filename); }
void SetSearchDirectoriesFromSettings(SettingsInterface& si);
bool IsDatabasePresent() const;
@ -79,15 +108,18 @@ public:
void AddDirectory(std::string path, bool recursive);
void Refresh(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress = nullptr);
void UpdateCompatibilityEntry(GameListCompatibilityEntry new_entry, bool save_to_list = true);
private:
enum : u32
{
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
GAME_LIST_CACHE_VERSION = 4
GAME_LIST_CACHE_VERSION = 5
};
using DatabaseMap = std::unordered_map<std::string, GameListDatabaseEntry>;
using CacheMap = std::unordered_map<std::string, GameListEntry>;
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
struct DirectoryEntry
{
@ -96,6 +128,7 @@ private:
};
class RedumpDatVisitor;
class CompatibilityListVisitor;
static bool GetExeListEntry(const char* path, GameListEntry* entry);
@ -109,18 +142,26 @@ private:
bool WriteEntryToCache(const GameListEntry* entry, ByteStream* stream);
void FlushCacheFileStream();
void CloseCacheFileStream();
void RewriteCacheFile();
void DeleteCacheFile();
void LoadDatabase();
void ClearDatabase();
void LoadCompatibilityList();
bool SaveCompatibilityDatabase();
bool SaveCompatibilityDatabaseForEntry(const GameListCompatibilityEntry* entry);
DatabaseMap m_database;
EntryList m_entries;
CacheMap m_cache_map;
CompatibilityMap m_compatibility_list;
std::unique_ptr<ByteStream> m_cache_write_stream;
std::vector<DirectoryEntry> m_search_directories;
std::string m_cache_filename;
std::string m_database_filename;
std::string m_compatibility_list_filename;
bool m_database_load_tried = false;
bool m_compatibility_list_load_tried = false;
};

View file

@ -43,8 +43,9 @@ bool HostInterface::Initialize()
m_settings.log_to_console, m_settings.log_to_debug, m_settings.log_to_window,
m_settings.log_to_file);
m_game_list = std::make_unique<GameList>();
m_game_list->SetCacheFilename(GetGameListCacheFileName());
m_game_list->SetDatabaseFilename(GetGameListDatabaseFileName());
m_game_list->SetCacheFilename(GetUserDirectoryRelativePath("cache/gamelist.cache"));
m_game_list->SetDatabaseFilename(GetUserDirectoryRelativePath("cache/redump.dat"));
m_game_list->SetCompatibilityFilename(GetUserDirectoryRelativePath("database/compatibility.xml"));
return true;
}
@ -766,16 +767,6 @@ std::string HostInterface::GetSettingsFileName() const
return GetUserDirectoryRelativePath("settings.ini");
}
std::string HostInterface::GetGameListCacheFileName() const
{
return GetUserDirectoryRelativePath("cache/gamelist.cache");
}
std::string HostInterface::GetGameListDatabaseFileName() const
{
return GetUserDirectoryRelativePath("cache/redump.dat");
}
std::string HostInterface::GetGameSaveStateFileName(const char* game_code, s32 slot) const
{
if (slot < 0)

View file

@ -206,12 +206,6 @@ protected:
/// Returns the path of the settings file.
std::string GetSettingsFileName() const;
/// Returns the path of the game list cache file.
std::string GetGameListCacheFileName() const;
/// Returns the path of the game database cache file.
std::string GetGameListDatabaseFileName() const;
/// Returns the path to a save state file. Specifying an index of -1 is the "resume" save state.
std::string GetGameSaveStateFileName(const char* game_code, s32 slot) const;

View file

@ -247,7 +247,7 @@ static std::array<const char*, 4> s_disc_region_display_names = {
std::optional<DiscRegion> Settings::ParseDiscRegionName(const char* str)
{
int index = 0;
for (const char* name : s_console_region_names)
for (const char* name : s_disc_region_names)
{
if (StringUtil::Strcasecmp(name, str) == 0)
return static_cast<DiscRegion>(index);