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

@ -9,6 +9,8 @@ add_library(frontend-common
fullscreen_ui.h
fullscreen_ui_progress_callback.cpp
fullscreen_ui_progress_callback.h
game_database.cpp
game_database.h
game_list.cpp
game_list.h
game_settings.cpp

View File

@ -85,7 +85,6 @@ bool CommonHostInterface::Initialize()
m_game_list = std::make_unique<GameList>();
m_game_list->SetCacheFilename(GetUserDirectoryRelativePath("cache/gamelist.cache"));
m_game_list->SetUserDatabaseFilename(GetUserDirectoryRelativePath("redump.dat"));
m_game_list->SetUserCompatibilityListFilename(GetUserDirectoryRelativePath("compatibility.xml"));
m_game_list->SetUserGameSettingsFilename(GetUserDirectoryRelativePath("gamesettings.ini"));
@ -2865,9 +2864,10 @@ void CommonHostInterface::GetGameInfo(const char* path, CDImage* image, std::str
if (image)
*code = System::GetGameCodeForImage(image, true);
const GameListDatabaseEntry* db_entry = (!code->empty()) ? m_game_list->GetDatabaseEntryForCode(*code) : nullptr;
if (db_entry)
*title = db_entry->title;
GameDatabase database;
GameDatabaseEntry database_entry;
if (database.Load() && database.GetEntryForDisc(image, &database_entry))
*title = std::move(database_entry.title);
else
*title = System::GetTitleForPath(path);
}
@ -2978,15 +2978,73 @@ bool CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, b
void CommonHostInterface::ApplyGameSettings(bool display_osd_messages)
{
g_settings.controller_disable_analog_mode_forcing = false;
// this gets called while booting, so can't use valid
if (System::IsShutdown() || System::GetRunningCode().empty() || !g_settings.apply_game_settings)
return;
const GameListEntry* ge = m_game_list->GetEntryForPath(System::GetRunningPath().c_str());
if (ge)
ApplyControllerCompatibilitySettings(ge->supported_controllers, display_osd_messages);
const GameSettings::Entry* gs = m_game_list->GetGameSettings(System::GetRunningPath(), System::GetRunningCode());
if (gs)
gs->ApplySettings(display_osd_messages);
}
void CommonHostInterface::ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages)
{
#define BIT_FOR(ctype) (static_cast<u64>(1) << static_cast<u32>(ctype))
if (controller_mask == 0 || controller_mask == static_cast<u64>(-1))
return;
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{
const ControllerType ctype = g_settings.controller_types[i];
if (ctype == ControllerType::None)
continue;
if (controller_mask & BIT_FOR(ctype))
continue;
// Special case: Dualshock is permitted when not supported as long as it's in digital mode.
if (ctype == ControllerType::AnalogController &&
(controller_mask & BIT_FOR(ControllerType::DigitalController)) != 0)
{
g_settings.controller_disable_analog_mode_forcing = true;
continue;
}
if (display_osd_messages)
{
SmallString supported_controller_string;
for (u32 j = 0; j < static_cast<u32>(ControllerType::Count); j++)
{
const ControllerType supported_ctype = static_cast<ControllerType>(j);
if ((controller_mask & BIT_FOR(supported_ctype)) == 0)
continue;
if (!supported_controller_string.IsEmpty())
supported_controller_string.AppendString(", ");
supported_controller_string.AppendString(
TranslateString("ControllerType", Settings::GetControllerTypeDisplayName(supported_ctype)));
}
AddFormattedOSDMessage(
30.0f,
TranslateString("OSDMessage", "Controller in port %u (%s) is not supported for %s.\nSupported controllers: "
"%s\nPlease configure a supported controller from the list above."),
i + 1u, TranslateString("ControllerType", Settings::GetControllerTypeDisplayName(ctype)).GetCharArray(),
System::GetRunningTitle().c_str(), supported_controller_string.GetCharArray());
}
}
#undef BIT_FOR
}
bool CommonHostInterface::UpdateControllerInputMapFromGameSettings()
{
// this gets called while booting, so can't use valid

View File

@ -15,6 +15,9 @@
class HostDisplayTexture;
class GameList;
struct GameDatabaseEntry;
class ControllerInterface;
namespace FrontendCommon {
@ -411,6 +414,7 @@ protected:
void RecreateSystem() override;
void ApplyGameSettings(bool display_osd_messages);
void ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages);
bool CreateHostDisplayResources();
void ReleaseHostDisplayResources();

View File

@ -93,6 +93,7 @@
<ClCompile Include="dinput_controller_interface.cpp" />
<ClCompile Include="fullscreen_ui.cpp" />
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
<ClCompile Include="game_database.cpp" />
<ClCompile Include="game_list.cpp" />
<ClCompile Include="game_settings.cpp" />
<ClCompile Include="http_downloader.cpp" />
@ -125,6 +126,7 @@
<ClInclude Include="dinput_controller_interface.h" />
<ClInclude Include="fullscreen_ui.h" />
<ClInclude Include="fullscreen_ui_progress_callback.h" />
<ClInclude Include="game_database.h" />
<ClInclude Include="game_list.h" />
<ClInclude Include="game_settings.h" />
<ClInclude Include="http_downloader.h" />

View File

@ -31,6 +31,7 @@
<ClCompile Include="http_downloader.cpp" />
<ClCompile Include="http_downloader_winhttp.cpp" />
<ClCompile Include="input_overlay_ui.cpp" />
<ClCompile Include="game_database.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="icon.h" />
@ -63,6 +64,7 @@
<ClInclude Include="http_downloader.h" />
<ClInclude Include="http_downloader_winhttp.h" />
<ClInclude Include="input_overlay_ui.h" />
<ClInclude Include="game_database.h" />
</ItemGroup>
<ItemGroup>
<None Include="font_roboto_regular.inl" />

View File

@ -2871,6 +2871,14 @@ void DrawGameListWindow()
ImGui::PushFont(g_medium_font);
// developer
if (!selected_entry->developer.empty())
{
text_width = ImGui::CalcTextSize(selected_entry->developer.c_str(), nullptr, false, work_width).x;
ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
ImGui::TextWrapped("%s", selected_entry->developer.c_str());
}
// code
text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x;
ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
@ -2885,6 +2893,14 @@ void DrawGameListWindow()
ImGui::SameLine();
ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region));
// genre
ImGui::Text("Genre: %s", selected_entry->genre.c_str());
// release date
char release_date_str[64];
selected_entry->GetReleaseDateString(release_date_str, sizeof(release_date_str));
ImGui::Text("Release Date: %s", release_date_str);
// compatibility
ImGui::TextUnformatted("Compatibility: ");
ImGui::SameLine();
@ -2896,9 +2912,6 @@ void DrawGameListWindow()
// size
ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);
// TODO: last played
ImGui::Text("Last Played: Never");
// game settings
const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount();
if (user_setting_count > 0)

View File

@ -0,0 +1,225 @@
#include "game_database.h"
#include "common/byte_stream.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/string_util.h"
#include "core/host_interface.h"
#include "core/system.h"
#include "rapidjson/document.h"
#include "rapidjson/error/en.h"
#include <iomanip>
Log_SetChannel(GameDatabase);
GameDatabase::GameDatabase() = default;
GameDatabase::~GameDatabase()
{
Unload();
}
bool GameDatabase::Load()
{
// TODO: use stream directly
std::unique_ptr<ByteStream> stream(
g_host_interface->OpenPackageFile("database/gamedb.json", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED));
if (!stream)
{
Log_ErrorPrintf("Failed to open game database");
return false;
}
std::string gamedb_data(FileSystem::ReadStreamToString(stream.get(), false));
if (gamedb_data.empty())
{
Log_ErrorPrintf("Failed to read game database");
return false;
}
std::unique_ptr<rapidjson::Document> json = std::make_unique<rapidjson::Document>();
json->Parse(gamedb_data.c_str(), gamedb_data.size());
if (json->HasParseError())
{
Log_ErrorPrintf("Failed to parse game database: %s at offset %zu",
rapidjson::GetParseError_En(json->GetParseError()), json->GetErrorOffset());
return false;
}
if (!json->IsArray())
{
Log_ErrorPrintf("Document is not an array");
return false;
}
m_json = json.release();
return true;
}
void GameDatabase::Unload()
{
if (m_json)
{
delete static_cast<rapidjson::Document*>(m_json);
m_json = nullptr;
}
}
static bool GetStringFromObject(const rapidjson::Value& object, const char* key, std::string* dest)
{
dest->clear();
auto member = object.FindMember(key);
if (member == object.MemberEnd() || !member->value.IsString())
return false;
dest->assign(member->value.GetString(), member->value.GetStringLength());
return true;
}
static bool GetUIntFromObject(const rapidjson::Value& object, const char* key, u32* dest)
{
*dest = 0;
auto member = object.FindMember(key);
if (member == object.MemberEnd() || !member->value.IsUint())
return false;
*dest = member->value.GetUint();
return true;
}
static const rapidjson::Value* FindDatabaseEntry(const std::string_view& code, rapidjson::Document* json)
{
for (const rapidjson::Value& current : json->GetArray())
{
if (!current.IsObject())
{
Log_WarningPrintf("entry is not an object");
continue;
}
auto member = current.FindMember("codes");
if (member == current.MemberEnd())
{
Log_WarningPrintf("codes member is missing");
continue;
}
if (!member->value.IsArray())
{
Log_WarningPrintf("codes is not an array");
continue;
}
for (const rapidjson::Value& current_code : member->value.GetArray())
{
if (!current_code.IsString())
{
Log_WarningPrintf("code is not a string");
continue;
}
if (StringUtil::Strncasecmp(current_code.GetString(), code.data(), code.length()) == 0)
return &current;
}
}
return nullptr;
}
bool GameDatabase::GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry)
{
if (!m_json)
return false;
const rapidjson::Value* object = FindDatabaseEntry(code, static_cast<rapidjson::Document*>(m_json));
if (!object)
return false;
if (!GetStringFromObject(*object, "serial", &entry->serial) || !GetStringFromObject(*object, "name", &entry->title))
{
Log_ErrorPrintf("Missing serial or title for entry");
return false;
}
GetStringFromObject(*object, "genre", &entry->genre);
GetStringFromObject(*object, "developer", &entry->developer);
GetStringFromObject(*object, "publisher", &entry->publisher);
GetUIntFromObject(*object, "minPlayers", &entry->min_players);
GetUIntFromObject(*object, "maxPlayers", &entry->max_players);
GetUIntFromObject(*object, "minBlocks", &entry->min_blocks);
GetUIntFromObject(*object, "maxBlocks", &entry->max_blocks);
entry->release_date = 0;
{
std::string release_date;
if (GetStringFromObject(*object, "releaseDate", &release_date))
{
std::istringstream iss(release_date);
struct tm parsed_time = {};
iss >> std::get_time(&parsed_time, "%Y-%m-%d");
if (!iss.fail())
{
parsed_time.tm_isdst = 0;
#ifdef _WIN32
entry->release_date = _mkgmtime(&parsed_time);
#else
entry->release_date = timegm(&parsed_time);
#endif
}
}
}
entry->supported_controllers_mask = ~0u;
auto controllers = object->FindMember("controllers");
if (controllers != object->MemberEnd())
{
if (controllers->value.IsArray())
{
bool first = true;
for (const rapidjson::Value& controller : controllers->value.GetArray())
{
if (!controller.IsString())
{
Log_WarningPrintf("controller is not a string");
return false;
}
std::optional<ControllerType> ctype = Settings::ParseControllerTypeName(controller.GetString());
if (!ctype.has_value())
{
Log_WarningPrintf("Invalid controller type '%s'", controller.GetString());
return false;
}
if (first)
{
entry->supported_controllers_mask = 0;
first = false;
}
entry->supported_controllers_mask |= (1u << static_cast<u32>(ctype.value()));
}
}
else
{
Log_WarningPrintf("controllers is not an array");
}
}
return true;
}
bool GameDatabase::GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry)
{
std::string exe_name_code(System::GetGameCodeForImage(image, false));
if (!exe_name_code.empty() && GetEntryForCode(exe_name_code, entry))
return true;
std::string exe_hash_code(System::GetGameHashCodeForImage(image));
if (!exe_hash_code.empty() && GetEntryForCode(exe_hash_code, entry))
return true;
Log_WarningPrintf("No entry found for disc (exe code: '%s', hash code: '%s')", exe_name_code.c_str(),
exe_hash_code.c_str());
return false;
}

View File

@ -0,0 +1,45 @@
#pragma once
#include "core/types.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
class CDImage;
struct GameDatabaseEntry
{
std::string serial;
std::string title;
std::string genre;
std::string developer;
std::string publisher;
u64 release_date;
u32 min_players;
u32 max_players;
u32 min_blocks;
u32 max_blocks;
u32 supported_controllers_mask;
};
class GameDatabase
{
public:
GameDatabase();
~GameDatabase();
bool Load();
void Unload();
bool GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry);
bool GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry);
bool GetTitleAndSerialForDisc(CDImage* image, GameDatabaseEntry* entry);
//bool Get
private:
void* m_json = nullptr;
};

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);
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "core/types.h"
#include "game_settings.h"
#include "game_database.h"
#include <memory>
#include <optional>
#include <string>
@ -34,24 +35,31 @@ enum class GameListCompatibilityRating
Count,
};
struct GameListDatabaseEntry
{
std::string code;
std::string title;
DiscRegion region;
};
struct GameListEntry
{
GameListEntryType type;
DiscRegion region;
std::string path;
std::string code;
std::string title;
std::string genre;
std::string publisher;
std::string developer;
u64 total_size;
u64 last_modified_time;
DiscRegion region;
GameListEntryType type;
u64 release_date;
u32 supported_controllers;
u8 min_players;
u8 max_players;
u8 min_blocks;
u8 max_blocks;
GameListCompatibilityRating compatibility_rating;
GameSettings::Entry settings;
size_t GetReleaseDateString(char* buffer, size_t buffer_size) const;
};
struct GameListCompatibilityEntry
@ -93,11 +101,11 @@ public:
const u32 GetSearchDirectoryCount() const { return static_cast<u32>(m_search_directories.size()); }
const GameListEntry* GetEntryForPath(const char* path) const;
const GameListDatabaseEntry* GetDatabaseEntryForCode(const std::string& code) const;
const GameListCompatibilityEntry* GetCompatibilityEntryForCode(const std::string& code) const;
bool GetGameCodeAndTitleFromDatabase(const char* path, std::string* code, std::string* title);
void SetCacheFilename(std::string filename) { m_cache_filename = std::move(filename); }
void SetUserDatabaseFilename(std::string filename) { m_user_database_filename = std::move(filename); }
void SetUserCompatibilityListFilename(std::string filename)
{
m_user_compatibility_list_filename = std::move(filename);
@ -123,10 +131,9 @@ private:
enum : u32
{
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
GAME_LIST_CACHE_VERSION = 25
GAME_LIST_CACHE_VERSION = 26
};
using DatabaseMap = std::unordered_map<std::string, GameListDatabaseEntry>;
using CacheMap = std::unordered_map<std::string, GameListEntry>;
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
@ -160,16 +167,15 @@ private:
void LoadGameSettings();
DatabaseMap m_database;
EntryList m_entries;
CacheMap m_cache_map;
GameDatabase m_database;
CompatibilityMap m_compatibility_list;
GameSettings::Database m_game_settings;
std::unique_ptr<ByteStream> m_cache_write_stream;
std::vector<DirectoryEntry> m_search_directories;
std::string m_cache_filename;
std::string m_user_database_filename;
std::string m_user_compatibility_list_filename;
std::string m_user_game_settings_filename;
bool m_database_load_tried = false;

View File

@ -33,7 +33,6 @@ std::array<std::pair<const char*, const char*>, static_cast<u32>(Trait::Count)>
{"DisablePGXPDepthBuffer", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Depth Buffer")},
{"ForcePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Force PGXP Vertex Cache")},
{"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")},
{"DisableAnalogModeForcing", TRANSLATABLE("GameSettingsTrait", "Disable Forcing Controller Analog Mode on Reset")},
{"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")},
{"ForceRecompilerICache", TRANSLATABLE("GameSettingsTrait", "Force Recompiler ICache")},
}};
@ -1240,9 +1239,6 @@ void Entry::ApplySettings(bool display_osd_messages) const
g_settings.gpu_pgxp_depth_buffer = false;
}
if (HasTrait(Trait::DisableAnalogModeForcing))
g_settings.controller_disable_analog_mode_forcing = true;
if (HasTrait(Trait::ForceRecompilerMemoryExceptions))
g_settings.cpu_recompiler_memory_exceptions = true;

View File

@ -25,7 +25,6 @@ enum class Trait : u32
DisablePGXPDepthBuffer,
ForcePGXPVertexCache,
ForcePGXPCPUMode,
DisableAnalogModeForcing,
ForceRecompilerMemoryExceptions,
ForceRecompilerICache,