From d7a1c447c6342c7a25e4961948b09f75ca6e2bac Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 4 Feb 2024 02:36:25 +1000 Subject: [PATCH] GameDatabase: Switch to YAML --- .github/workflows/gamedb-lint.yml | 15 +- extras/yamllint-config.yaml | 16 + src/core/CMakeLists.txt | 2 +- src/core/core.props | 6 +- src/core/core.vcxproj | 3 + src/core/game_database.cpp | 724 +++++++++++------------ src/core/game_database.h | 16 +- src/duckstation-qt/gamesummarywidget.cpp | 38 +- src/duckstation-qt/gamesummarywidget.ui | 6 +- 9 files changed, 426 insertions(+), 400 deletions(-) create mode 100644 extras/yamllint-config.yaml diff --git a/.github/workflows/gamedb-lint.yml b/.github/workflows/gamedb-lint.yml index 342a6971f..2dd4095f7 100644 --- a/.github/workflows/gamedb-lint.yml +++ b/.github/workflows/gamedb-lint.yml @@ -3,13 +3,15 @@ name: GameDB Lint on: pull_request: paths: - - 'data/resources/gamedb.json' + - 'data/resources/gamedb.yaml' + - 'data/resources/discdb.yaml' push: branches: - master - dev paths: - - 'data/resources/gamedb.json' + - 'data/resources/gamedb.yaml' + - 'data/resources/discdb.yaml' workflow_dispatch: jobs: @@ -25,9 +27,12 @@ jobs: shell: bash run: | sudo apt-get update - sudo apt-get -y install python3-demjson + sudo apt-get -y install yamllint - name: Check GameDB shell: bash - run: | - jsonlint -s data/resources/gamedb.json + run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/gamedb.yaml + + - name: Check DiscDB + shell: bash + run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/discdb.yaml diff --git a/extras/yamllint-config.yaml b/extras/yamllint-config.yaml new file mode 100644 index 000000000..def770861 --- /dev/null +++ b/extras/yamllint-config.yaml @@ -0,0 +1,16 @@ +extends: default + +rules: + line-length: + max: 200 + indentation: + spaces: 2 + indent-sequences: true + document-start: + present: false + document-end: + present: false + comments: + require-starting-space: true + min-spaces-from-content: 1 + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e685cfaf6..11e1ded95 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -131,7 +131,7 @@ target_precompile_headers(core PRIVATE "pch.h") target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_link_libraries(core PUBLIC Threads::Threads common util ZLIB::ZLIB) -target_link_libraries(core PRIVATE stb xxhash imgui rapidjson rcheevos) +target_link_libraries(core PRIVATE stb xxhash imgui rapidyaml rcheevos) if(CPU_ARCH_X64) target_compile_definitions(core PUBLIC "ENABLE_RECOMPILER=1" "ENABLE_NEWREC=1" "ENABLE_MMAP_FASTMEM=1") diff --git a/src/core/core.props b/src/core/core.props index 3d34ca16e..7fbaca20b 100644 --- a/src/core/core.props +++ b/src/core/core.props @@ -10,7 +10,11 @@ ENABLE_MMAP_FASTMEM=1;%(PreprocessorDefinitions) ENABLE_NEWREC=1;%(PreprocessorDefinitions) - %(AdditionalIncludeDirectories);$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)dep\discord-rpc\include + %(AdditionalIncludeDirectories);$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\discord-rpc\include + + %(PreprocessorDefinitions);C4_NO_DEBUG_BREAK=1 + %(AdditionalIncludeDirectories);$(SolutionDir)dep\rapidyaml\include;$(SolutionDir)dep\rapidjson\include + %(AdditionalIncludeDirectories);$(SolutionDir)dep\rainterface %(AdditionalIncludeDirectories);$(SolutionDir)dep\xbyak\xbyak diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 345a0aa94..31428f491 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -172,6 +172,9 @@ {e4357877-d459-45c7-b8f6-dcbb587bb528} + + {1ad23a8a-4c20-434c-ae6b-0e07759eeb1e} + {4ba0a6d4-3ae1-42b2-9347-096fd023ff64} diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp index 08b2fecf8..054416a01 100644 --- a/src/core/game_database.cpp +++ b/src/core/game_database.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "game_database.h" @@ -16,22 +16,18 @@ #include "common/string_util.h" #include "common/timer.h" -#include "rapidjson/document.h" -#include "rapidjson/error/en.h" +#include "ryml.hpp" #include #include #include #include +#include #include "IconsFontAwesome5.h" Log_SetChannel(GameDatabase); -#ifdef _WIN32 -#include "common/windows_headers.h" -#endif - namespace GameDatabase { enum : u32 @@ -46,35 +42,51 @@ static const Entry* GetEntryForId(const std::string_view& code); static bool LoadFromCache(); static bool SaveToCache(); -static bool LoadGameDBJson(); -static bool ParseJsonEntry(Entry* entry, const rapidjson::Value& value); -static bool ParseJsonCodes(u32 index, const rapidjson::Value& value); +static void SetRymlCallbacks(); +static bool LoadGameDBYaml(); +static bool ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value); +static bool ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial); static bool LoadTrackHashes(); -static std::array(GameDatabase::Trait::Count)> s_trait_names = {{ - "ForceInterpreter", - "ForceSoftwareRenderer", - "ForceSoftwareRendererForReadbacks", - "ForceInterlacing", - "DisableTrueColor", - "DisableUpscaling", - "DisableTextureFiltering", - "DisableScaledDithering", - "DisableForceNTSCTimings", - "DisableWidescreen", - "DisablePGXP", - "DisablePGXPCulling", - "DisablePGXPTextureCorrection", - "DisablePGXPColorCorrection", - "DisablePGXPDepthBuffer", - "ForcePGXPVertexCache", - "ForcePGXPCPUMode", - "ForceRecompilerMemoryExceptions", - "ForceRecompilerICache", - "ForceRecompilerLUTFastmem", - "IsLibCryptProtected", +static constexpr const std::array(CompatibilityRating::Count)> + s_compatibility_rating_names = { + {"Unknown", "DoesntBoot", "CrashesInIntro", "CrashesInGame", "GraphicalAudioIssues", "NoIssues"}}; + +static constexpr const std::array(CompatibilityRating::Count)> + s_compatibility_rating_display_names = {{TRANSLATE_NOOP("GameListCompatibilityRating", "Unknown"), + TRANSLATE_NOOP("GameListCompatibilityRating", "Doesn't Boot"), + TRANSLATE_NOOP("GameListCompatibilityRating", "Crashes In Intro"), + TRANSLATE_NOOP("GameListCompatibilityRating", "Crashes In-Game"), + TRANSLATE_NOOP("GameListCompatibilityRating", "Graphical/Audio Issues"), + TRANSLATE_NOOP("GameListCompatibilityRating", "No Issues")}}; + +static constexpr const std::array(GameDatabase::Trait::Count)> s_trait_names = {{ + "forceInterpreter", + "forceSoftwareRenderer", + "forceSoftwareRendererForReadbacks", + "forceInterlacing", + "disableTrueColor", + "disableUpscaling", + "disableTextureFiltering", + "disableScaledDithering", + "disableForceNTSCTimings", + "disableWidescreen", + "disablePGXP", + "disablePGXPCulling", + "disablePGXPTextureCorrection", + "disablePGXPColorCorrection", + "disablePGXPDepthBuffer", + "forcePGXPVertexCache", + "forcePGXPCPUMode", + "forceRecompilerMemoryExceptions", + "forceRecompilerICache", + "forceRecompilerLUTFastmem", + "isLibCryptProtected", }}; +static constexpr const char* GAMEDB_YAML_FILENAME = "gamedb.yaml"; +static constexpr const char* DISCDB_YAML_FILENAME = "discdb.yaml"; + static bool s_loaded = false; static bool s_track_hashes_loaded = false; @@ -84,6 +96,94 @@ static PreferUnorderedStringMap s_code_lookup; static TrackHashesMap s_track_hashes_map; } // namespace GameDatabase +// RapidYAML utility routines. + +ALWAYS_INLINE std::string_view to_stringview(const c4::csubstr& s) +{ + return std::string_view(s.data(), s.size()); +} + +ALWAYS_INLINE std::string_view to_stringview(const c4::substr& s) +{ + return std::string_view(s.data(), s.size()); +} + +ALWAYS_INLINE c4::csubstr to_csubstr(const std::string_view& sv) +{ + return c4::csubstr(sv.data(), sv.length()); +} + +static bool GetStringFromObject(const ryml::ConstNodeRef& object, std::string_view key, std::string* dest) +{ + dest->clear(); + + const ryml::ConstNodeRef member = object.find_child(to_csubstr(key)); + if (!member.valid()) + return false; + + const c4::csubstr val = member.val(); + if (!val.empty()) + dest->assign(val.data(), val.size()); + + return true; +} + +template +static bool GetUIntFromObject(const ryml::ConstNodeRef& object, std::string_view key, T* dest) +{ + *dest = 0; + + const ryml::ConstNodeRef member = object.find_child(to_csubstr(key)); + if (!member.valid()) + return false; + + const c4::csubstr val = member.val(); + if (val.empty()) + { + Log_ErrorFmt("Unexpected empty value in {}", key); + return false; + } + + const std::optional opt_value = StringUtil::FromChars(to_stringview(val)); + if (!opt_value.has_value()) + { + Log_ErrorFmt("Unexpected non-uint value in {}", key); + return false; + } + + *dest = opt_value.value(); + return true; +} + +template +static std::optional GetOptionalTFromObject(const ryml::ConstNodeRef& object, std::string_view key) +{ + std::optional ret; + + const ryml::ConstNodeRef member = object.find_child(to_csubstr(key)); + if (member.valid()) + { + const c4::csubstr val = member.val(); + if (!val.empty()) + { + ret = StringUtil::FromChars(to_stringview(val)); + if (!ret.has_value()) + { + if constexpr (std::is_floating_point_v) + Log_ErrorFmt("Unexpected non-float value in {}", key); + else if constexpr (std::is_integral_v) + Log_ErrorFmt("Unexpected non-int value in {}", key); + } + } + else + { + Log_ErrorFmt("Unexpected empty value in {}", key); + } + } + + return ret; +} + void GameDatabase::EnsureLoaded() { if (s_loaded) @@ -98,11 +198,11 @@ void GameDatabase::EnsureLoaded() s_entries = {}; s_code_lookup = {}; - LoadGameDBJson(); + LoadGameDBYaml(); SaveToCache(); } - Log_InfoPrintf("Database load took %.2f ms", timer.GetTimeMilliseconds()); + Log_InfoFmt("Database load took {:.0f}ms", timer.GetTimeMilliseconds()); } void GameDatabase::Unload() @@ -198,30 +298,16 @@ GameDatabase::Entry* GameDatabase::GetMutableEntry(const std::string_view& seria return nullptr; } -const char* GameDatabase::GetTraitName(Trait trait) -{ - DebugAssert(trait < Trait::Count); - return s_trait_names[static_cast(trait)]; -} - const char* GameDatabase::GetCompatibilityRatingName(CompatibilityRating rating) { - static std::array(CompatibilityRating::Count)> names = { - {"Unknown", "DoesntBoot", "CrashesInIntro", "CrashesInGame", "GraphicalAudioIssues", "NoIssues"}}; - return names[static_cast(rating)]; + return s_compatibility_rating_names[static_cast(rating)]; } const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating rating) { - static constexpr std::array(CompatibilityRating::Count)> names = { - {TRANSLATE_NOOP("GameListCompatibilityRating", "Unknown"), - TRANSLATE_NOOP("GameListCompatibilityRating", "Doesn't Boot"), - TRANSLATE_NOOP("GameListCompatibilityRating", "Crashes In Intro"), - TRANSLATE_NOOP("GameListCompatibilityRating", "Crashes In-Game"), - TRANSLATE_NOOP("GameListCompatibilityRating", "Graphical/Audio Issues"), - TRANSLATE_NOOP("GameListCompatibilityRating", "No Issues")}}; return (rating >= CompatibilityRating::Unknown && rating < CompatibilityRating::Count) ? - Host::TranslateToCString("GameListCompatibilityRating", names[static_cast(rating)]) : + Host::TranslateToCString("GameListCompatibilityRating", + s_compatibility_rating_display_names[static_cast(rating)]) : ""; } @@ -580,7 +666,7 @@ bool GameDatabase::LoadFromCache() return false; } - const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.json", false).value_or(0); + const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0); u32 signature, version, num_entries, num_codes; u64 file_gamedb_ts; @@ -674,7 +760,7 @@ bool GameDatabase::LoadFromCache() bool GameDatabase::SaveToCache() { - const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.json", false).value_or(0); + const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0); std::unique_ptr stream( ByteStream::OpenFile(GetCacheFile().c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_WRITE | @@ -742,329 +828,258 @@ bool GameDatabase::SaveToCache() return true; } -////////////////////////////////////////////////////////////////////////// -// JSON Parsing -////////////////////////////////////////////////////////////////////////// - -static bool GetStringFromObject(const rapidjson::Value& object, const char* key, std::string* dest) +void GameDatabase::SetRymlCallbacks() { - 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; + ryml::Callbacks callbacks = ryml::get_callbacks(); + callbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void* userdata) { + Log_ErrorFmt("Parse error at {}:{} (bufpos={}): {}", loc.line, loc.col, loc.offset, std::string_view(msg, msg_len)); + }; + ryml::set_callbacks(callbacks); + c4::set_error_callback( + [](const char* msg, size_t msg_size) { Log_ErrorFmt("C4 error: {}", std::string_view(msg, msg_size)); }); } -static bool GetBoolFromObject(const rapidjson::Value& object, const char* key, bool* dest) +bool GameDatabase::LoadGameDBYaml() { - *dest = false; + Common::Timer timer; - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsBool()) - return false; - - *dest = member->value.GetBool(); - return true; -} - -template -static bool GetUIntFromObject(const rapidjson::Value& object, const char* key, T* dest) -{ - *dest = 0; - - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsUint()) - return false; - - *dest = static_cast(member->value.GetUint()); - return true; -} - -static bool GetArrayOfStringsFromObject(const rapidjson::Value& object, const char* key, std::vector* dest) -{ - dest->clear(); - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsArray()) - return false; - - for (const rapidjson::Value& str : member->value.GetArray()) - { - if (str.IsString()) - { - dest->emplace_back(str.GetString(), str.GetStringLength()); - } - } - return true; -} - -template -static std::optional GetOptionalIntFromObject(const rapidjson::Value& object, const char* key) -{ - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsInt()) - return std::nullopt; - - return static_cast(member->value.GetInt()); -} - -template -static std::optional GetOptionalUIntFromObject(const rapidjson::Value& object, const char* key) -{ - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsUint()) - return std::nullopt; - - return static_cast(member->value.GetUint()); -} - -static std::optional GetOptionalFloatFromObject(const rapidjson::Value& object, const char* key) -{ - auto member = object.FindMember(key); - if (member == object.MemberEnd() || !member->value.IsFloat()) - return std::nullopt; - - return member->value.GetFloat(); -} - -bool GameDatabase::LoadGameDBJson() -{ - std::optional gamedb_data(Host::ReadResourceFileToString("gamedb.json", false)); + const std::optional gamedb_data = Host::ReadResourceFileToString(GAMEDB_YAML_FILENAME, false); if (!gamedb_data.has_value()) { - Log_ErrorPrintf("Failed to read game database"); + Log_ErrorPrint("Failed to read game database"); return false; } - // TODO: Parse in-place, avoid string allocations. - std::unique_ptr json = std::make_unique(); - 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; - } + SetRymlCallbacks(); - if (!json->IsArray()) - { - Log_ErrorPrintf("Document is not an array"); - return false; - } + const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(GAMEDB_YAML_FILENAME), to_csubstr(gamedb_data.value())); + const ryml::ConstNodeRef root = tree.rootref(); + s_entries.reserve(root.num_children()); - const auto& jarray = json->GetArray(); - s_entries.reserve(jarray.Size()); - - for (const rapidjson::Value& current : json->GetArray()) + for (const ryml::ConstNodeRef& current : root.children()) { // TODO: binary sort const u32 index = static_cast(s_entries.size()); Entry& entry = s_entries.emplace_back(); - if (!ParseJsonEntry(&entry, current)) + if (!ParseYamlEntry(&entry, current)) { s_entries.pop_back(); continue; } - ParseJsonCodes(index, current); + ParseYamlCodes(index, current, entry.serial); } - Log_InfoPrintf("Loaded %zu entries and %zu codes from database", s_entries.size(), s_code_lookup.size()); - return true; + ryml::reset_callbacks(); + + Log_InfoFmt("Loaded {} entries and {} codes from database in {:.0f}ms.", s_entries.size(), s_code_lookup.size(), + timer.GetTimeMilliseconds()); + return !s_entries.empty(); } -bool GameDatabase::ParseJsonEntry(Entry* entry, const rapidjson::Value& value) +bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value) { - if (!value.IsObject()) + entry->serial = to_stringview(value.key()); + if (entry->serial.empty()) { - Log_WarningPrintf("entry is not an object"); + Log_ErrorPrint("Missing serial for entry."); return false; } - if (!GetStringFromObject(value, "serial", &entry->serial) || !GetStringFromObject(value, "name", &entry->title) || - entry->serial.empty()) + GetStringFromObject(value, "name", &entry->title); + + if (const ryml::ConstNodeRef metadata = value.find_child(to_csubstr("metadata")); metadata.valid()) { - Log_ErrorPrintf("Missing serial or title for entry"); - return false; - } + GetStringFromObject(metadata, "genre", &entry->genre); + GetStringFromObject(metadata, "developer", &entry->developer); + GetStringFromObject(metadata, "publisher", &entry->publisher); - GetStringFromObject(value, "genre", &entry->genre); - GetStringFromObject(value, "developer", &entry->developer); - GetStringFromObject(value, "publisher", &entry->publisher); + GetUIntFromObject(metadata, "minPlayers", &entry->min_players); + GetUIntFromObject(metadata, "maxPlayers", &entry->max_players); + GetUIntFromObject(metadata, "minBlocks", &entry->min_blocks); + GetUIntFromObject(metadata, "maxBlocks", &entry->max_blocks); - GetUIntFromObject(value, "minPlayers", &entry->min_players); - GetUIntFromObject(value, "maxPlayers", &entry->max_players); - GetUIntFromObject(value, "minBlocks", &entry->min_blocks); - GetUIntFromObject(value, "maxBlocks", &entry->max_blocks); - - entry->release_date = 0; - { - std::string release_date; - if (GetStringFromObject(value, "releaseDate", &release_date)) + entry->release_date = 0; { - std::istringstream iss(release_date); - struct tm parsed_time = {}; - iss >> std::get_time(&parsed_time, "%Y-%m-%d"); - if (!iss.fail()) + std::string release_date; + if (GetStringFromObject(metadata, "releaseDate", &release_date)) { - parsed_time.tm_isdst = 0; + 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); + entry->release_date = _mkgmtime(&parsed_time); #else - entry->release_date = timegm(&parsed_time); + entry->release_date = timegm(&parsed_time); #endif + } } } } entry->supported_controllers = static_cast(~0u); - const auto controllers = value.FindMember("controllers"); - if (controllers != value.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 ctype = Settings::ParseControllerTypeName(controller.GetString()); - if (!ctype.has_value()) + if (const ryml::ConstNodeRef controllers = value.find_child(to_csubstr("controllers")); + controllers.valid() && controllers.has_children()) + { + bool first = true; + for (const ryml::ConstNodeRef& controller : controllers.children()) + { + const std::string_view controller_str = to_stringview(controller.val()); + if (controller_str.empty()) + { + Log_WarningFmt("controller is not a string in {}", entry->serial); + return false; + } + + std::optional ctype = Settings::ParseControllerTypeName(controller_str); + if (!ctype.has_value()) + { + Log_WarningFmt("Invalid controller type {} in {}", controller_str, entry->serial); + continue; + } + + if (first) + { + entry->supported_controllers = 0; + first = false; + } + + entry->supported_controllers |= (1u << static_cast(ctype.value())); + } + } + + if (const ryml::ConstNodeRef compatibility = value.find_child(to_csubstr("compatibility")); + compatibility.valid() && compatibility.has_children()) + { + const ryml::ConstNodeRef rating = compatibility.find_child(to_csubstr("rating")); + if (rating.valid()) + { + const std::string_view rating_str = to_stringview(rating.val()); + + const auto iter = std::find(s_compatibility_rating_names.begin(), s_compatibility_rating_names.end(), rating_str); + if (iter != s_compatibility_rating_names.end()) + { + const size_t rating_idx = static_cast(std::distance(s_compatibility_rating_names.begin(), iter)); + DebugAssert(rating_idx < static_cast(CompatibilityRating::Count)); + entry->compatibility = static_cast(rating_idx); + } + else + { + Log_WarningFmt("Unknown compatibility rating {} in {}", rating_str, entry->serial); + } + } + } + + if (const ryml::ConstNodeRef traits = value.find_child(to_csubstr("traits")); traits.valid() && traits.has_children()) + { + for (const ryml::ConstNodeRef& trait : traits.children()) + { + const std::string_view trait_str = to_stringview(trait.val()); + if (trait_str.empty()) + { + Log_WarningFmt("Empty trait in {}", entry->serial); + continue; + } + + const auto iter = std::find(s_trait_names.begin(), s_trait_names.end(), trait_str); + if (iter == s_trait_names.end()) + { + Log_WarningFmt("Unknown trait {} in {}", trait_str, entry->serial); + continue; + } + + const size_t trait_idx = static_cast(std::distance(s_trait_names.begin(), iter)); + DebugAssert(trait_idx < static_cast(Trait::Count)); + entry->traits[trait_idx] = true; + } + } + + if (const ryml::ConstNodeRef settings = value.find_child(to_csubstr("settings")); + settings.valid() && settings.has_children()) + { + entry->display_active_start_offset = GetOptionalTFromObject(settings, "displayActiveStartOffset"); + entry->display_active_end_offset = GetOptionalTFromObject(settings, "displayActiveEndOffset"); + entry->display_line_start_offset = GetOptionalTFromObject(settings, "displayLineStartOffset"); + entry->display_line_end_offset = GetOptionalTFromObject(settings, "displayLineEndOffset"); + entry->dma_max_slice_ticks = GetOptionalTFromObject(settings, "dmaMaxSliceTicks"); + entry->dma_halt_ticks = GetOptionalTFromObject(settings, "dmaHaltTicks"); + entry->gpu_fifo_size = GetOptionalTFromObject(settings, "gpuFIFOSize"); + entry->gpu_max_run_ahead = GetOptionalTFromObject(settings, "gpuMaxRunAhead"); + entry->gpu_pgxp_tolerance = GetOptionalTFromObject(settings, "gpuPGXPTolerance"); + entry->gpu_pgxp_depth_threshold = GetOptionalTFromObject(settings, "gpuPGXPDepthThreshold"); + } + + if (const ryml::ConstNodeRef disc_set = value.find_child("discSet"); disc_set.valid() && disc_set.has_children()) + { + GetStringFromObject(disc_set, "name", &entry->disc_set_name); + + if (const ryml::ConstNodeRef set_serials = disc_set.find_child("serials"); + set_serials.valid() && set_serials.has_children()) + { + entry->disc_set_serials.reserve(set_serials.num_children()); + for (const ryml::ConstNodeRef& serial : set_serials) + { + const std::string_view serial_str = to_stringview(serial.val()); + if (serial_str.empty()) { - Log_WarningPrintf("Invalid controller type '%s'", controller.GetString()); + Log_WarningFmt("Empty disc set serial in {}", entry->serial); continue; } - if (first) + if (std::find(entry->disc_set_serials.begin(), entry->disc_set_serials.end(), serial_str) != + entry->disc_set_serials.end()) { - entry->supported_controllers = 0; - first = false; + Log_WarningFmt("Duplicate serial {} in disc set serials for {}", serial_str, entry->serial); + continue; } - entry->supported_controllers |= (1u << static_cast(ctype.value())); + entry->disc_set_serials.emplace_back(serial_str); } } - else - { - Log_WarningPrintf("controllers is not an array"); - } - } - - const auto compatibility = value.FindMember("compatibility"); - if (compatibility != value.MemberEnd()) - { - if (compatibility->value.IsObject()) - { - u32 rating; - if (GetUIntFromObject(compatibility->value, "rating", &rating) && - rating < static_cast(CompatibilityRating::Count)) - { - entry->compatibility = static_cast(rating); - } - } - else - { - Log_WarningPrintf("compatibility is not an object"); - } - } - - const auto traits = value.FindMember("traits"); - if (traits != value.MemberEnd()) - { - if (traits->value.IsObject()) - { - const auto& traitsobj = traits->value; - for (u32 trait = 0; trait < static_cast(Trait::Count); trait++) - { - bool bvalue; - if (GetBoolFromObject(traitsobj, s_trait_names[trait], &bvalue) && bvalue) - entry->traits[trait] = bvalue; - } - - entry->display_active_start_offset = GetOptionalIntFromObject(traitsobj, "DisplayActiveStartOffset"); - entry->display_active_end_offset = GetOptionalIntFromObject(traitsobj, "DisplayActiveEndOffset"); - entry->display_line_start_offset = GetOptionalIntFromObject(traitsobj, "DisplayLineStartOffset"); - entry->display_line_end_offset = GetOptionalIntFromObject(traitsobj, "DisplayLineEndOffset"); - entry->dma_max_slice_ticks = GetOptionalUIntFromObject(traitsobj, "DMAMaxSliceTicks"); - entry->dma_halt_ticks = GetOptionalUIntFromObject(traitsobj, "DMAHaltTicks"); - entry->gpu_fifo_size = GetOptionalUIntFromObject(traitsobj, "GPUFIFOSize"); - entry->gpu_max_run_ahead = GetOptionalUIntFromObject(traitsobj, "GPUMaxRunAhead"); - entry->gpu_pgxp_tolerance = GetOptionalFloatFromObject(traitsobj, "GPUPGXPTolerance"); - entry->gpu_pgxp_depth_threshold = GetOptionalFloatFromObject(traitsobj, "GPUPGXPDepthThreshold"); - } - else - { - Log_WarningPrintf("traits is not an object"); - } - } - - GetStringFromObject(value, "discSetName", &entry->disc_set_name); - const auto disc_set_serials = value.FindMember("discSetSerials"); - if (disc_set_serials != value.MemberEnd()) - { - if (disc_set_serials->value.IsArray()) - { - const auto disc_set_serials_array = disc_set_serials->value.GetArray(); - entry->disc_set_serials.reserve(disc_set_serials_array.Size()); - for (const rapidjson::Value& serial : disc_set_serials_array) - { - if (serial.IsString()) - { - entry->disc_set_serials.emplace_back(serial.GetString(), serial.GetStringLength()); - } - else - { - Log_WarningPrintf("discSetSerial is not a string"); - } - } - } - else - { - Log_WarningPrintf("discSetSerials is not an array"); - } } return true; } -bool GameDatabase::ParseJsonCodes(u32 index, const rapidjson::Value& value) +bool GameDatabase::ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial) { - auto member = value.FindMember("codes"); - if (member == value.MemberEnd()) + const ryml::ConstNodeRef& codes = value.find_child(to_csubstr("codes")); + if (!codes.valid() || !codes.has_children()) { - Log_WarningPrintf("codes member is missing"); - return false; - } + // use serial instead + auto iter = s_code_lookup.find(serial); + if (iter != s_code_lookup.end()) + { + Log_WarningFmt("Duplicate code '{}'", serial); + return false; + } - if (!member->value.IsArray()) - { - Log_WarningPrintf("codes is not an array"); - return false; + s_code_lookup.emplace(serial, index); + return true; } u32 added = 0; - for (const rapidjson::Value& current_code : member->value.GetArray()) + for (const ryml::ConstNodeRef& current_code : codes) { - if (!current_code.IsString()) + const std::string_view current_code_str = to_stringview(current_code.val()); + if (current_code_str.empty()) { - Log_WarningPrintf("code is not a string"); + Log_WarningFmt("code is not a string in {}", serial); continue; } - const std::string_view code(current_code.GetString(), current_code.GetStringLength()); - auto iter = s_code_lookup.find(code); + auto iter = s_code_lookup.find(current_code_str); if (iter != s_code_lookup.end()) { - Log_WarningPrintf("Duplicate code '%.*s'", static_cast(code.size()), code.data()); + Log_WarningFmt("Duplicate code '{}' in {}", current_code_str, serial); continue; } - s_code_lookup.emplace(code, index); + s_code_lookup.emplace(current_code_str, index); added++; } @@ -1082,105 +1097,84 @@ void GameDatabase::EnsureTrackHashesMapLoaded() bool GameDatabase::LoadTrackHashes() { - std::optional gamedb_data(Host::ReadResourceFileToString("gamedb.json", false)); + Common::Timer load_timer; + + std::optional gamedb_data(Host::ReadResourceFileToString(DISCDB_YAML_FILENAME, false)); if (!gamedb_data.has_value()) { - Log_ErrorPrintf("Failed to read game database"); + Log_ErrorPrint("Failed to read game database"); return false; } + SetRymlCallbacks(); + // TODO: Parse in-place, avoid string allocations. - std::unique_ptr json = std::make_unique(); - 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; - } + const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(DISCDB_YAML_FILENAME), to_csubstr(gamedb_data.value())); + const ryml::ConstNodeRef root = tree.rootref(); s_track_hashes_map = {}; - for (const rapidjson::Value& current : json->GetArray()) + size_t serials = 0; + for (const ryml::ConstNodeRef& current : root.children()) { - if (!current.IsObject()) + const std::string_view serial = to_stringview(current.key()); + if (serial.empty() || !current.has_children()) { - Log_WarningPrintf("entry is not an object"); + Log_WarningPrint("entry is not an object"); continue; } - std::vector codes; - if (!GetArrayOfStringsFromObject(current, "codes", &codes)) + const ryml::ConstNodeRef track_data = current.find_child(to_csubstr("trackData")); + if (!track_data.valid() || !track_data.has_children()) { - Log_WarningPrintf("codes member is missing"); + Log_WarningFmt("trackData is missing in {}", serial); continue; } - auto track_data = current.FindMember("track_data"); - if (track_data == current.MemberEnd()) + u32 revision = 0; + for (const ryml::ConstNodeRef& track_revisions : track_data.children()) { - Log_WarningPrintf("track_data member is missing"); - continue; - } - - if (!track_data->value.IsArray()) - { - Log_WarningPrintf("track_data is not an array"); - continue; - } - - uint32_t revision = 0; - for (const rapidjson::Value& track_revisions : track_data->value.GetArray()) - { - if (!track_revisions.IsObject()) + const ryml::ConstNodeRef tracks = track_revisions.find_child(to_csubstr("tracks")); + if (!tracks.valid() || !tracks.has_children()) { - Log_WarningPrintf("track_data is not an array of object"); + Log_WarningFmt("tracks member is missing in {}", serial); continue; } - auto tracks = track_revisions.FindMember("tracks"); - if (tracks == track_revisions.MemberEnd()) - { - Log_WarningPrintf("tracks member is missing"); - continue; - } + std::string revision_string; + GetStringFromObject(track_revisions, "version", &revision_string); - if (!tracks->value.IsArray()) + for (const ryml::ConstNodeRef& track : tracks) { - Log_WarningPrintf("tracks is not an array"); - continue; - } - - std::string revisionString; - GetStringFromObject(track_revisions, "version", &revisionString); - - for (const rapidjson::Value& track : tracks->value.GetArray()) - { - auto md5_field = track.FindMember("md5"); - if (md5_field == track.MemberEnd() || !md5_field->value.IsString()) + const ryml::ConstNodeRef md5 = track.find_child("md5"); + std::string_view md5_str; + if (!md5.valid() || (md5_str = to_stringview(md5.val())).empty()) { + Log_WarningFmt("md5 is missing in track in {}", serial); continue; } - auto md5 = CDImageHasher::HashFromString( - std::string_view(md5_field->value.GetString(), md5_field->value.GetStringLength())); - if (md5) + const std::optional md5o = CDImageHasher::HashFromString(md5_str); + if (md5o.has_value()) { - s_track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5.value()), - std::forward_as_tuple(codes, revisionString, revision)); + s_track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5o.value()), + std::forward_as_tuple(std::string(serial), revision_string, revision)); + } + else + { + Log_WarningFmt("invalid md5 in {}", serial); } } revision++; } + + serials++; } - return true; + ryml::reset_callbacks(); + Log_InfoFmt("Loaded {} track hashes from {} serials in {:.0f}ms.", s_track_hashes_map.size(), serials, + load_timer.GetTimeMilliseconds()); + return !s_track_hashes_map.empty(); } const GameDatabase::TrackHashesMap& GameDatabase::GetTrackHashesMap() diff --git a/src/core/game_database.h b/src/core/game_database.h index 06e2578d6..81b4ad68c 100644 --- a/src/core/game_database.h +++ b/src/core/game_database.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -98,16 +98,14 @@ const Entry* GetEntryForSerial(const std::string_view& serial); std::string GetSerialForDisc(CDImage* image); std::string GetSerialForPath(const char* path); -const char* GetTraitName(Trait trait); - const char* GetCompatibilityRatingName(CompatibilityRating rating); const char* GetCompatibilityRatingDisplayName(CompatibilityRating rating); /// Map of track hashes for image verification struct TrackData { - TrackData(std::vector codes, std::string revisionString, uint32_t revision) - : codes(std::move(codes)), revisionString(revisionString), revision(revision) + TrackData(std::string serial_, std::string revision_str_, uint32_t revision_) + : serial(std::move(serial_)), revision_str(std::move(revision_str_)), revision(revision_) { } @@ -115,12 +113,12 @@ struct TrackData { // 'revisionString' is deliberately ignored in comparisons as it's redundant with comparing 'revision'! Do not // change! - return left.codes == right.codes && left.revision == right.revision; + return left.serial == right.serial && left.revision == right.revision; } - std::vector codes; - std::string revisionString; - uint32_t revision; + std::string serial; + std::string revision_str; + u32 revision; }; using TrackHashesMap = std::multimap; diff --git a/src/duckstation-qt/gamesummarywidget.cpp b/src/duckstation-qt/gamesummarywidget.cpp index adce80a11..f66baaf44 100644 --- a/src/duckstation-qt/gamesummarywidget.cpp +++ b/src/duckstation-qt/gamesummarywidget.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "gamesummarywidget.h" @@ -13,7 +13,6 @@ #include "fmt/format.h" -#include #include #include @@ -219,13 +218,6 @@ void GameSummaryWidget::onComputeHashClicked() return; } -#ifndef _DEBUGFAST - // Kick off hash preparation asynchronously, as building the map of results may take a while - // This breaks for DebugFast because of the iterator debug level mismatch. - QFuture result = - QtConcurrent::run([]() { return &GameDatabase::GetTrackHashesMap(); }); -#endif - QtModalProgressCallback progress_callback(this); progress_callback.SetProgressRange(image->GetTrackCount()); @@ -259,6 +251,7 @@ void GameSummaryWidget::onComputeHashClicked() if (calculate_hash_success) { std::string found_revision; + std::string found_serial; m_redump_search_keyword = CDImageHasher::HashToString(track_hashes.front()); progress_callback.SetStatusText("Verifying hashes..."); @@ -270,11 +263,7 @@ void GameSummaryWidget::onComputeHashClicked() // 2. For each data track match, try to match all audio tracks // If all match, assume this revision. Else, try other revisions, // and accept the one with the most matches. -#ifndef _DEBUGFAST - const GameDatabase::TrackHashesMap& hashes_map = *result.result(); -#else const GameDatabase::TrackHashesMap& hashes_map = GameDatabase::GetTrackHashesMap(); -#endif auto data_track_matches = hashes_map.equal_range(track_hashes[0]); if (data_track_matches.first != data_track_matches.second) @@ -317,13 +306,30 @@ void GameSummaryWidget::onComputeHashClicked() } } - found_revision = best_data_match->second.revisionString; + found_revision = best_data_match->second.revision_str; + found_serial = best_data_match->second.serial; } + QString text; + if (!found_revision.empty()) + text = tr("Revision: %1").arg(found_revision.empty() ? tr("N/A") : QString::fromStdString(found_revision)); + + if (found_serial != m_ui.serial->text().toStdString()) { - m_ui.revision->setText( - tr("Revision: %1").arg(found_revision.empty() ? tr("N/A") : QString::fromStdString(found_revision))); + text = + tr("Serial Mismatch: %1 vs %2%3").arg(QString::fromStdString(found_serial)).arg(m_ui.serial->text()).arg(text); + } + + if (!text.isEmpty()) + { + if (m_ui.verifySpacer) + { + m_ui.verifyLayout->removeItem(m_ui.verifySpacer); + delete m_ui.verifySpacer; + m_ui.verifySpacer = nullptr; + } + m_ui.revision->setText(text); m_ui.revision->setVisible(true); } } diff --git a/src/duckstation-qt/gamesummarywidget.ui b/src/duckstation-qt/gamesummarywidget.ui index 5e71a699b..5c8e047c0 100644 --- a/src/duckstation-qt/gamesummarywidget.ui +++ b/src/duckstation-qt/gamesummarywidget.ui @@ -189,9 +189,9 @@ - + - + Qt::Horizontal @@ -207,7 +207,7 @@ - 100 + 300 0