// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "game_database.h" #include "controller.h" #include "host.h" #include "system.h" #include "util/cd_image.h" #include "util/imgui_manager.h" #include "common/assert.h" #include "common/byte_stream.h" #include "common/heterogeneous_containers.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "common/timer.h" #include "ryml.hpp" #include #include #include #include #include #include "IconsFontAwesome5.h" Log_SetChannel(GameDatabase); namespace GameDatabase { enum : u32 { GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48, GAME_DATABASE_CACHE_VERSION = 12, }; static Entry* GetMutableEntry(std::string_view serial); static const Entry* GetEntryForId(std::string_view code); static bool LoadFromCache(); static bool SaveToCache(); 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 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("GameDatabase", "Unknown"), TRANSLATE_NOOP("GameDatabase", "Doesn't Boot"), TRANSLATE_NOOP("GameDatabase", "Crashes In Intro"), TRANSLATE_NOOP("GameDatabase", "Crashes In-Game"), TRANSLATE_NOOP("GameDatabase", "Graphical/Audio Issues"), TRANSLATE_NOOP("GameDatabase", "No Issues")}}; static constexpr const std::array(GameDatabase::Trait::Count)> s_trait_names = {{ "ForceInterpreter", "ForceSoftwareRenderer", "ForceSoftwareRendererForReadbacks", "ForceRoundTextureCoordinates", "ForceAccurateBlending", "ForceInterlacing", "DisableTrueColor", "DisableUpscaling", "DisableTextureFiltering", "DisableScaledDithering", "DisableForceNTSCTimings", "DisableWidescreen", "DisablePGXP", "DisablePGXPCulling", "DisablePGXPTextureCorrection", "DisablePGXPColorCorrection", "DisablePGXPDepthBuffer", "DisablePGXPPreserveProjFP", "DisablePGXPOn2DPolygons", "ForcePGXPVertexCache", "ForcePGXPCPUMode", "ForceRecompilerMemoryExceptions", "ForceRecompilerICache", "ForceRecompilerLUTFastmem", "IsLibCryptProtected", }}; static constexpr const std::array(GameDatabase::Trait::Count)> s_trait_display_names = {{ TRANSLATE_NOOP("GameDatabase", "Force Interpreter"), TRANSLATE_NOOP("GameDatabase", "Force Software Renderer"), TRANSLATE_NOOP("GameDatabase", "Force Software Renderer For Readbacks"), TRANSLATE_NOOP("GameDatabase", "Force Round Texture Coordinates"), TRANSLATE_NOOP("GameDatabase", "Force Interlacing"), TRANSLATE_NOOP("GameDatabase", "Disable True Color"), TRANSLATE_NOOP("GameDatabase", "Disable Upscaling"), TRANSLATE_NOOP("GameDatabase", "Disable Texture Filtering"), TRANSLATE_NOOP("GameDatabase", "Disable Scaled Dithering"), TRANSLATE_NOOP("GameDatabase", "Disable Force NTSC Timings"), TRANSLATE_NOOP("GameDatabase", "Disable Widescreen"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP Culling"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP Texture Correction"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP Color Correction"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP Depth Buffer"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP Preserve Projection Floating Point"), TRANSLATE_NOOP("GameDatabase", "Disable PGXP on 2D Polygons"), TRANSLATE_NOOP("GameDatabase", "Force PGXP Vertex Cache"), TRANSLATE_NOOP("GameDatabase", "Force PGXP CPU Mode"), TRANSLATE_NOOP("GameDatabase", "Force Recompiler Memory Exceptions"), TRANSLATE_NOOP("GameDatabase", "Force Recompiler ICache"), TRANSLATE_NOOP("GameDatabase", "Force Recompiler LUT Fastmem"), TRANSLATE_NOOP("GameDatabase", "Is LibCrypt Protected"), }}; 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; static std::vector s_entries; 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(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()) { ERROR_LOG("Unexpected empty value in {}", key); return false; } const std::optional opt_value = StringUtil::FromChars(to_stringview(val)); if (!opt_value.has_value()) { ERROR_LOG("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) ERROR_LOG("Unexpected non-float value in {}", key); else if constexpr (std::is_integral_v) ERROR_LOG("Unexpected non-int value in {}", key); } } else { ERROR_LOG("Unexpected empty value in {}", key); } } return ret; } template static std::optional ParseOptionalTFromObject(const ryml::ConstNodeRef& object, std::string_view key, std::optional (*from_string_function)(const char* str)) { 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 = from_string_function(TinyString(to_stringview(val))); if (!ret.has_value()) ERROR_LOG("Unknown value for {}: {}", key, to_stringview(val)); } else { ERROR_LOG("Unexpected empty value in {}", key); } } return ret; } void GameDatabase::EnsureLoaded() { if (s_loaded) return; Common::Timer timer; s_loaded = true; if (!LoadFromCache()) { s_entries = {}; s_code_lookup = {}; LoadGameDBYaml(); SaveToCache(); } INFO_LOG("Database load of {} entries took {:.0f}ms.", s_entries.size(), timer.GetTimeMilliseconds()); } void GameDatabase::Unload() { s_entries = {}; s_code_lookup = {}; s_loaded = false; } const GameDatabase::Entry* GameDatabase::GetEntryForId(std::string_view code) { if (code.empty()) return nullptr; EnsureLoaded(); auto iter = s_code_lookup.find(code); return (iter != s_code_lookup.end()) ? &s_entries[iter->second] : nullptr; } std::string GameDatabase::GetSerialForDisc(CDImage* image) { std::string ret; const GameDatabase::Entry* entry = GetEntryForDisc(image); if (entry) ret = entry->serial; return ret; } std::string GameDatabase::GetSerialForPath(const char* path) { std::string ret; if (System::IsLoadableFilename(path) && !System::IsExeFileName(path) && !System::IsPsfFileName(path)) { std::unique_ptr image(CDImage::Open(path, false, nullptr)); if (image) ret = GetSerialForDisc(image.get()); } return ret; } const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image) { std::string id; System::GameHash hash; System::GetGameDetailsFromImage(image, &id, &hash); const Entry* entry = GetEntryForGameDetails(id, hash); if (entry) return entry; WARNING_LOG("No entry found for disc '{}'", id); return nullptr; } const GameDatabase::Entry* GameDatabase::GetEntryForGameDetails(const std::string& id, u64 hash) { const Entry* entry; if (!id.empty()) { entry = GetEntryForId(id); if (entry) return entry; } // some games with invalid serials use the hash entry = GetEntryForId(System::GetGameHashId(hash)); if (entry) return entry; return nullptr; } const GameDatabase::Entry* GameDatabase::GetEntryForSerial(std::string_view serial) { EnsureLoaded(); return GetMutableEntry(serial); } GameDatabase::Entry* GameDatabase::GetMutableEntry(std::string_view serial) { for (Entry& entry : s_entries) { if (entry.serial == serial) return &entry; } return nullptr; } const char* GameDatabase::GetTraitName(Trait trait) { return s_trait_names[static_cast(trait)]; } const char* GameDatabase::GetTraitDisplayName(Trait trait) { return Host::TranslateToCString("GameDatabase", s_trait_display_names[static_cast(trait)]); } const char* GameDatabase::GetCompatibilityRatingName(CompatibilityRating rating) { return s_compatibility_rating_names[static_cast(rating)]; } const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating rating) { return (rating >= CompatibilityRating::Unknown && rating < CompatibilityRating::Count) ? Host::TranslateToCString("GameDatabase", s_compatibility_rating_display_names[static_cast(rating)]) : ""; } void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_messages) const { if (display_active_start_offset.has_value()) { settings.display_active_start_offset = display_active_start_offset.value(); if (display_osd_messages) INFO_LOG("GameDB: Display active start offset set to {}.", settings.display_active_start_offset); } if (display_active_end_offset.has_value()) { settings.display_active_end_offset = display_active_end_offset.value(); if (display_osd_messages) INFO_LOG("GameDB: Display active end offset set to {}.", settings.display_active_end_offset); } if (display_line_start_offset.has_value()) { settings.display_line_start_offset = display_line_start_offset.value(); if (display_osd_messages) INFO_LOG("GameDB: Display line start offset set to {}.", settings.display_line_start_offset); } if (display_line_end_offset.has_value()) { settings.display_line_end_offset = display_line_end_offset.value(); if (display_osd_messages) INFO_LOG("GameDB: Display line end offset set to {}.", settings.display_line_start_offset); } if (display_deinterlacing_mode.has_value()) { settings.display_deinterlacing_mode = display_deinterlacing_mode.value(); if (display_osd_messages) { INFO_LOG("GameDB: Display deinterlacing mode set to {}.", Settings::GetDisplayDeinterlacingModeName(settings.display_deinterlacing_mode)); } } if (dma_max_slice_ticks.has_value()) { settings.dma_max_slice_ticks = dma_max_slice_ticks.value(); if (display_osd_messages) INFO_LOG("GameDB: DMA max slice ticks set to {}.", settings.dma_max_slice_ticks); } if (dma_halt_ticks.has_value()) { settings.dma_halt_ticks = dma_halt_ticks.value(); if (display_osd_messages) INFO_LOG("GameDB: DMA halt ticks set to {}.", settings.dma_halt_ticks); } if (gpu_fifo_size.has_value()) { settings.gpu_fifo_size = gpu_fifo_size.value(); if (display_osd_messages) INFO_LOG("GameDB: GPU FIFO size set to {}.", settings.gpu_fifo_size); } if (gpu_max_run_ahead.has_value()) { settings.gpu_max_run_ahead = gpu_max_run_ahead.value(); if (display_osd_messages) INFO_LOG("GameDB: GPU max runahead set to {}.", settings.gpu_max_run_ahead); } if (gpu_pgxp_tolerance.has_value()) { settings.gpu_pgxp_tolerance = gpu_pgxp_tolerance.value(); if (display_osd_messages) INFO_LOG("GameDB: GPU PGXP tolerance set to {}.", settings.gpu_pgxp_tolerance); } if (gpu_pgxp_depth_threshold.has_value()) { settings.SetPGXPDepthClearThreshold(gpu_pgxp_depth_threshold.value()); if (display_osd_messages) INFO_LOG("GameDB: GPU depth clear threshold set to {}.", settings.GetPGXPDepthClearThreshold()); } if (gpu_line_detect_mode.has_value()) { settings.gpu_line_detect_mode = gpu_line_detect_mode.value(); if (display_osd_messages) { INFO_LOG("GameDB: GPU line detect mode set to {}.", Settings::GetLineDetectModeName(settings.gpu_line_detect_mode)); } } SmallStackString<512> messages; #define APPEND_MESSAGE(icon, msg) \ do \ { \ messages.append("\n \u2022 "); \ messages.append(msg); \ } while (0) if (HasTrait(Trait::ForceInterpreter)) { if (display_osd_messages && settings.cpu_execution_mode != CPUExecutionMode::Interpreter) APPEND_MESSAGE(ICON_FA_MICROCHIP, TRANSLATE_SV("GameDatabase", "CPU recompiler disabled.")); settings.cpu_execution_mode = CPUExecutionMode::Interpreter; } if (HasTrait(Trait::ForceSoftwareRenderer)) { if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software) APPEND_MESSAGE(ICON_FA_PAINT_ROLLER, TRANSLATE_SV("GameDatabase", "Hardware rendering disabled.")); settings.gpu_renderer = GPURenderer::Software; } if (HasTrait(Trait::ForceSoftwareRendererForReadbacks)) { if (display_osd_messages && settings.gpu_renderer != GPURenderer::Software) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "Software renderer readbacks enabled.")); settings.gpu_use_software_renderer_for_readbacks = true; } if (HasTrait(Trait::ForceRoundUpscaledTextureCoordinates)) { settings.gpu_force_round_texcoords = true; } if (HasTrait(Trait::ForceAccurateBlending)) { if (display_osd_messages && !settings.IsUsingSoftwareRenderer() && !settings.gpu_accurate_blending) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "Accurate blending enabled.")); settings.gpu_accurate_blending = true; } if (HasTrait(Trait::ForceInterlacing)) { if (display_osd_messages && settings.gpu_disable_interlacing) APPEND_MESSAGE(ICON_FA_TV, TRANSLATE_SV("GameDatabase", "Interlaced rendering enabled.")); settings.gpu_disable_interlacing = false; } if (HasTrait(Trait::DisableTrueColor)) { if (display_osd_messages && settings.gpu_true_color) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "True color disabled.")); settings.gpu_true_color = false; } if (HasTrait(Trait::DisableUpscaling)) { if (display_osd_messages && settings.gpu_resolution_scale > 1) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "Upscaling disabled.")); settings.gpu_resolution_scale = 1; } if (HasTrait(Trait::DisableTextureFiltering)) { if (display_osd_messages && (settings.gpu_texture_filter != GPUTextureFilter::Nearest || g_settings.gpu_sprite_texture_filter != GPUTextureFilter::Nearest)) { APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "Texture filtering disabled.")); } settings.gpu_texture_filter = GPUTextureFilter::Nearest; settings.gpu_sprite_texture_filter = GPUTextureFilter::Nearest; } if (HasTrait(Trait::DisableScaledDithering)) { if (display_osd_messages && settings.gpu_scaled_dithering) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "Scaled dithering.")); settings.gpu_scaled_dithering = false; } if (HasTrait(Trait::DisableWidescreen)) { if (display_osd_messages && settings.gpu_widescreen_hack) APPEND_MESSAGE(ICON_FA_TV, TRANSLATE_SV("GameDatabase", "Widescreen rendering disabled.")); settings.gpu_widescreen_hack = false; } if (HasTrait(Trait::DisableForceNTSCTimings)) { if (display_osd_messages && settings.gpu_force_ntsc_timings) APPEND_MESSAGE(ICON_FA_TV, TRANSLATE_SV("GameDatabase", "Force NTSC timings disabled.")); settings.gpu_force_ntsc_timings = false; } if (HasTrait(Trait::DisablePGXP)) { if (display_osd_messages && settings.gpu_pgxp_enable) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP geometry correction disabled.")); settings.gpu_pgxp_enable = false; } if (HasTrait(Trait::DisablePGXPCulling)) { if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_culling) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP culling correction disabled.")); settings.gpu_pgxp_culling = false; } if (HasTrait(Trait::DisablePGXPTextureCorrection)) { if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP perspective correct textures disabled.")); settings.gpu_pgxp_texture_correction = false; } if (HasTrait(Trait::DisablePGXPColorCorrection)) { if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_texture_correction && settings.gpu_pgxp_color_correction) { APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP perspective correct colors disabled.")); } settings.gpu_pgxp_color_correction = false; } if (HasTrait(Trait::DisablePGXPPreserveProjFP)) { if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_preserve_proj_fp) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP preserve projection precision disabled.")); settings.gpu_pgxp_preserve_proj_fp = false; } if (HasTrait(Trait::ForcePGXPVertexCache)) { if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_vertex_cache) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP vertex cache enabled.")); settings.gpu_pgxp_vertex_cache = settings.gpu_pgxp_enable; } else if (settings.gpu_pgxp_enable && settings.gpu_pgxp_vertex_cache) { Host::AddIconOSDMessage( "gamedb_force_pgxp_vertex_cache", ICON_FA_EXCLAMATION_TRIANGLE, TRANSLATE_STR( "GameDatabase", "PGXP Vertex Cache is enabled, but it is not required for this game. This may cause rendering errors."), Host::OSD_WARNING_DURATION); } if (HasTrait(Trait::ForcePGXPCPUMode)) { if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_cpu) { #ifndef __ANDROID__ APPEND_MESSAGE(ICON_FA_MICROCHIP, TRANSLATE_SV("GameDatabase", "PGXP CPU mode enabled.")); #else Host::AddIconOSDMessage( "gamedb_force_pgxp_cpu", ICON_FA_MICROCHIP, "This game requires PGXP CPU mode, which increases system requirements.\n" ICON_FA_EXCLAMATION_TRIANGLE " If the game runs too slow, disable PGXP for this game.", Host::OSD_WARNING_DURATION); #endif } settings.gpu_pgxp_cpu = settings.gpu_pgxp_enable; } else if (settings.UsingPGXPCPUMode()) { Host::AddIconOSDMessage( "gamedb_force_pgxp_cpu", ICON_FA_MICROCHIP, TRANSLATE_STR("GameDatabase", "PGXP CPU mode is enabled, but it is not required for this game. This may cause rendering errors."), Host::OSD_WARNING_DURATION); } if (HasTrait(Trait::DisablePGXPDepthBuffer)) { if (display_osd_messages && settings.gpu_pgxp_enable && settings.gpu_pgxp_depth_buffer) APPEND_MESSAGE(ICON_FA_MAGIC, TRANSLATE_SV("GameDatabase", "PGXP depth buffer disabled.")); settings.gpu_pgxp_depth_buffer = false; } if (HasTrait(Trait::DisablePGXPOn2DPolygons)) { if (display_osd_messages && settings.gpu_pgxp_enable && !settings.gpu_pgxp_disable_2d) APPEND_MESSAGE(ICON_FA_MICROCHIP, TRANSLATE_SV("GameDatabase", "PGXP disabled on 2D polygons.")); g_settings.gpu_pgxp_disable_2d = true; } if (HasTrait(Trait::ForceRecompilerMemoryExceptions)) { WARNING_LOG("Memory exceptions for recompiler forced by compatibility settings."); settings.cpu_recompiler_memory_exceptions = true; } if (HasTrait(Trait::ForceRecompilerICache)) { WARNING_LOG("ICache for recompiler forced by compatibility settings."); settings.cpu_recompiler_icache = true; } if (settings.cpu_fastmem_mode == CPUFastmemMode::MMap && HasTrait(Trait::ForceRecompilerLUTFastmem)) { WARNING_LOG("LUT fastmem for recompiler forced by compatibility settings."); settings.cpu_fastmem_mode = CPUFastmemMode::LUT; } if (!messages.empty()) { Host::AddIconOSDMessage( "GameDBCompatibility", ICON_FA_MICROCHIP, fmt::format("{}{}", TRANSLATE_SV("GameDatabase", "Compatibility settings for this game have been applied:"), messages.view()), Host::OSD_WARNING_DURATION); } #undef APPEND_MESSAGE #define BIT_FOR(ctype) (static_cast(1) << static_cast(ctype)) if (supported_controllers != 0 && supported_controllers != static_cast(-1)) { for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { const ControllerType ctype = settings.controller_types[i]; if (ctype == ControllerType::None) continue; if (supported_controllers & BIT_FOR(ctype)) continue; // Special case: Dualshock is permitted when not supported as long as it's in digital mode. if (ctype == ControllerType::AnalogController && (supported_controllers & BIT_FOR(ControllerType::DigitalController)) != 0) { settings.controller_disable_analog_mode_forcing = true; continue; } if (display_osd_messages) { SmallString supported_controller_string; for (u32 j = 0; j < static_cast(ControllerType::Count); j++) { const ControllerType supported_ctype = static_cast(j); if ((supported_controllers & BIT_FOR(supported_ctype)) == 0) continue; if (!supported_controller_string.empty()) supported_controller_string.append(", "); supported_controller_string.append(Controller::GetControllerInfo(supported_ctype)->GetDisplayName()); } Host::AddKeyedOSDMessage( "gamedb_controller_unsupported", fmt::format(TRANSLATE_FS("GameDatabase", "Controller in port {0} ({1}) is not supported for {2}.\nSupported controllers: " "{3}\nPlease configure a supported controller from the list above."), i + 1u, Controller::GetControllerInfo(ctype)->GetDisplayName(), System::GetGameTitle(), supported_controller_string), Host::OSD_CRITICAL_ERROR_DURATION); } } } #undef BIT_FOR } template static inline void AppendIntegerSetting(SmallStringBase& str, bool& heading, std::string_view title, const std::optional& value) { if (!value.has_value()) return; if (!heading) { heading = true; str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings")); } str.append_format(" - {}: {}\n", title, value.value()); } static inline void AppendFloatSetting(SmallStringBase& str, bool& heading, std::string_view title, const std::optional& value) { if (!value.has_value()) return; if (!heading) { heading = true; str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings")); } str.append_format(" - {}: {:.2f}\n", title, value.value()); } template static inline void AppendEnumSetting(SmallStringBase& str, bool& heading, std::string_view title, const char* (*get_display_name_func)(T), const std::optional& value) { if (!value.has_value()) return; if (!heading) { heading = true; str.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Settings")); } str.append_format(" - {}: {}\n", title, get_display_name_func(value.value())); } std::string GameDatabase::Entry::GenerateCompatibilityReport() const { LargeString ret; ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Title"), title); ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Serial"), serial); ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Rating"), GetCompatibilityRatingDisplayName(compatibility)); if (!compatibility_version_tested.empty()) ret.append_format("**{}:**\n{}\n\n", TRANSLATE_SV("GameDatabase", "Version Tested"), compatibility_version_tested); if (!compatibility_comments.empty()) ret.append_format("**{}**\n\n{}\n\n", TRANSLATE_SV("GameDatabase", "Comments"), compatibility_comments); if (supported_controllers != 0) { ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Supported Controllers")); for (u32 j = 0; j < static_cast(ControllerType::Count); j++) { if ((supported_controllers & (static_cast(1) << j)) == 0) continue; ret.append_format(" - {}\n", Controller::GetControllerInfo(static_cast(j))->GetDisplayName()); } ret.append("\n"); } if (traits.any()) { ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Traits")); for (u32 i = 0; i < static_cast(Trait::Count); i++) { if (traits.test(i)) ret.append_format(" - {}\n", GetTraitDisplayName(static_cast(i))); } ret.append("\n"); } bool settings_heading = false; AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active Start Offset"), display_active_start_offset); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Active End Offset"), display_active_end_offset); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line Start Offset"), display_line_start_offset); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Line End Offset"), display_line_end_offset); AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "Display Deinterlacing Mode"), &Settings::GetDisplayDeinterlacingModeDisplayName, display_deinterlacing_mode); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Max Slice Ticks"), dma_max_slice_ticks); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "DMA Halt Ticks"), dma_halt_ticks); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU FIFO Size"), gpu_fifo_size); AppendIntegerSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Max Runahead"), gpu_max_run_ahead); AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU PGXP Tolerance"), gpu_pgxp_tolerance); AppendFloatSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU PGXP Depth Threshold"), gpu_pgxp_depth_threshold); AppendEnumSetting(ret, settings_heading, TRANSLATE_SV("GameDatabase", "GPU Line Detect Mode"), &Settings::GetLineDetectModeDisplayName, gpu_line_detect_mode); if (!disc_set_name.empty()) { ret.append_format("**{}:** {}\n", TRANSLATE_SV("GameDatabase", "Disc Set"), disc_set_name); for (const std::string& ds_serial : disc_set_serials) ret.append_format(" - {}\n", ds_serial); } return std::string(ret.view()); } template bool ReadOptionalFromStream(ByteStream* stream, std::optional* dest) { bool has_value; if (!stream->Read2(&has_value, sizeof(has_value))) return false; if (!has_value) return true; T value; if (!stream->Read2(&value, sizeof(T))) return false; *dest = value; return true; } template bool WriteOptionalToStream(ByteStream* stream, const std::optional& src) { const bool has_value = src.has_value(); if (!stream->Write2(&has_value, sizeof(has_value))) return false; if (!has_value) return true; return stream->Write2(&src.value(), sizeof(T)); } static std::string GetCacheFile() { return Path::Combine(EmuFolders::Cache, "gamedb.cache"); } bool GameDatabase::LoadFromCache() { std::unique_ptr stream( ByteStream::OpenFile(GetCacheFile().c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED)); if (!stream) { DEV_LOG("Cache does not exist, loading full database."); return false; } const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0); u32 signature, version, num_entries, num_codes; u64 file_gamedb_ts; if (!stream->ReadU32(&signature) || !stream->ReadU32(&version) || !stream->ReadU64(&file_gamedb_ts) || !stream->ReadU32(&num_entries) || !stream->ReadU32(&num_codes) || signature != GAME_DATABASE_CACHE_SIGNATURE || version != GAME_DATABASE_CACHE_VERSION) { DEV_LOG("Cache header is corrupted or version mismatch."); return false; } if (gamedb_ts != file_gamedb_ts) { DEV_LOG("Cache is out of date, recreating."); return false; } s_entries.reserve(num_entries); for (u32 i = 0; i < num_entries; i++) { Entry& entry = s_entries.emplace_back(); constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8; std::array bits; u8 compatibility; u32 num_disc_set_serials; if (!stream->ReadSizePrefixedString(&entry.serial) || !stream->ReadSizePrefixedString(&entry.title) || !stream->ReadSizePrefixedString(&entry.genre) || !stream->ReadSizePrefixedString(&entry.developer) || !stream->ReadSizePrefixedString(&entry.publisher) || !stream->ReadSizePrefixedString(&entry.compatibility_version_tested) || !stream->ReadSizePrefixedString(&entry.compatibility_comments) || !stream->ReadU64(&entry.release_date) || !stream->ReadU8(&entry.min_players) || !stream->ReadU8(&entry.max_players) || !stream->ReadU8(&entry.min_blocks) || !stream->ReadU8(&entry.max_blocks) || !stream->ReadU16(&entry.supported_controllers) || !stream->ReadU8(&compatibility) || compatibility >= static_cast(GameDatabase::CompatibilityRating::Count) || !stream->Read2(bits.data(), num_bytes) || !ReadOptionalFromStream(stream.get(), &entry.display_active_start_offset) || !ReadOptionalFromStream(stream.get(), &entry.display_active_end_offset) || !ReadOptionalFromStream(stream.get(), &entry.display_line_start_offset) || !ReadOptionalFromStream(stream.get(), &entry.display_line_end_offset) || !ReadOptionalFromStream(stream.get(), &entry.display_deinterlacing_mode) || !ReadOptionalFromStream(stream.get(), &entry.dma_max_slice_ticks) || !ReadOptionalFromStream(stream.get(), &entry.dma_halt_ticks) || !ReadOptionalFromStream(stream.get(), &entry.gpu_fifo_size) || !ReadOptionalFromStream(stream.get(), &entry.gpu_max_run_ahead) || !ReadOptionalFromStream(stream.get(), &entry.gpu_pgxp_tolerance) || !ReadOptionalFromStream(stream.get(), &entry.gpu_pgxp_depth_threshold) || !ReadOptionalFromStream(stream.get(), &entry.gpu_line_detect_mode) || !stream->ReadSizePrefixedString(&entry.disc_set_name) || !stream->ReadU32(&num_disc_set_serials)) { DEV_LOG("Cache entry is corrupted."); return false; } if (num_disc_set_serials > 0) { entry.disc_set_serials.reserve(num_disc_set_serials); for (u32 j = 0; j < num_disc_set_serials; j++) { if (!stream->ReadSizePrefixedString(&entry.disc_set_serials.emplace_back())) { DEV_LOG("Cache entry is corrupted."); return false; } } } entry.compatibility = static_cast(compatibility); entry.traits.reset(); for (u32 j = 0; j < static_cast(Trait::Count); j++) { if ((bits[j / 8] & (1u << (j % 8))) != 0) entry.traits[j] = true; } } for (u32 i = 0; i < num_codes; i++) { std::string code; u32 index; if (!stream->ReadSizePrefixedString(&code) || !stream->ReadU32(&index) || index >= static_cast(s_entries.size())) { DEV_LOG("Cache code entry is corrupted."); return false; } s_code_lookup.emplace(std::move(code), index); } return true; } bool GameDatabase::SaveToCache() { 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 | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_STREAMED)); if (!stream) return false; bool result = stream->WriteU32(GAME_DATABASE_CACHE_SIGNATURE); result = result && stream->WriteU32(GAME_DATABASE_CACHE_VERSION); result = result && stream->WriteU64(static_cast(gamedb_ts)); result = result && stream->WriteU32(static_cast(s_entries.size())); result = result && stream->WriteU32(static_cast(s_code_lookup.size())); for (const Entry& entry : s_entries) { result = result && stream->WriteSizePrefixedString(entry.serial); result = result && stream->WriteSizePrefixedString(entry.title); result = result && stream->WriteSizePrefixedString(entry.genre); result = result && stream->WriteSizePrefixedString(entry.developer); result = result && stream->WriteSizePrefixedString(entry.publisher); result = result && stream->WriteSizePrefixedString(entry.compatibility_version_tested); result = result && stream->WriteSizePrefixedString(entry.compatibility_comments); result = result && stream->WriteU64(entry.release_date); result = result && stream->WriteU8(entry.min_players); result = result && stream->WriteU8(entry.max_players); result = result && stream->WriteU8(entry.min_blocks); result = result && stream->WriteU8(entry.max_blocks); result = result && stream->WriteU16(entry.supported_controllers); result = result && stream->WriteU8(static_cast(entry.compatibility)); constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8; std::array bits; bits.fill(0); for (u32 j = 0; j < static_cast(Trait::Count); j++) { if (entry.traits[j]) bits[j / 8] |= (1u << (j % 8)); } result = result && stream->Write2(bits.data(), num_bytes); result = result && WriteOptionalToStream(stream.get(), entry.display_active_start_offset); result = result && WriteOptionalToStream(stream.get(), entry.display_active_end_offset); result = result && WriteOptionalToStream(stream.get(), entry.display_line_start_offset); result = result && WriteOptionalToStream(stream.get(), entry.display_line_end_offset); result = result && WriteOptionalToStream(stream.get(), entry.display_deinterlacing_mode); result = result && WriteOptionalToStream(stream.get(), entry.dma_max_slice_ticks); result = result && WriteOptionalToStream(stream.get(), entry.dma_halt_ticks); result = result && WriteOptionalToStream(stream.get(), entry.gpu_fifo_size); result = result && WriteOptionalToStream(stream.get(), entry.gpu_max_run_ahead); result = result && WriteOptionalToStream(stream.get(), entry.gpu_pgxp_tolerance); result = result && WriteOptionalToStream(stream.get(), entry.gpu_pgxp_depth_threshold); result = result && WriteOptionalToStream(stream.get(), entry.gpu_line_detect_mode); result = result && stream->WriteSizePrefixedString(entry.disc_set_name); result = result && stream->WriteU32(static_cast(entry.disc_set_serials.size())); for (const std::string& serial : entry.disc_set_serials) result = result && stream->WriteSizePrefixedString(serial); } for (const auto& it : s_code_lookup) { result = result && stream->WriteSizePrefixedString(it.first); result = result && stream->WriteU32(it.second); } result = result && stream->Flush(); return true; } void GameDatabase::SetRymlCallbacks() { ryml::Callbacks callbacks = ryml::get_callbacks(); callbacks.m_error = [](const char* msg, size_t msg_len, ryml::Location loc, void* userdata) { ERROR_LOG("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) { ERROR_LOG("C4 error: {}", std::string_view(msg, msg_size)); }); } bool GameDatabase::LoadGameDBYaml() { const std::optional gamedb_data = Host::ReadResourceFileToString(GAMEDB_YAML_FILENAME, false); if (!gamedb_data.has_value()) { ERROR_LOG("Failed to read game database"); return false; } SetRymlCallbacks(); 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()); 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 (!ParseYamlEntry(&entry, current)) { s_entries.pop_back(); continue; } ParseYamlCodes(index, current, entry.serial); } ryml::reset_callbacks(); return !s_entries.empty(); } bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value) { entry->serial = to_stringview(value.key()); if (entry->serial.empty()) { ERROR_LOG("Missing serial for entry."); return false; } GetStringFromObject(value, "name", &entry->title); if (const ryml::ConstNodeRef metadata = value.find_child(to_csubstr("metadata")); metadata.valid()) { GetStringFromObject(metadata, "genre", &entry->genre); GetStringFromObject(metadata, "developer", &entry->developer); GetStringFromObject(metadata, "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); entry->release_date = 0; { std::string release_date; if (GetStringFromObject(metadata, "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 = static_cast(~0u); 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()) { WARNING_LOG("controller is not a string in {}", entry->serial); return false; } const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(controller_str); if (!cinfo) { WARNING_LOG("Invalid controller type {} in {}", controller_str, entry->serial); continue; } if (first) { entry->supported_controllers = 0; first = false; } entry->supported_controllers |= (1u << static_cast(cinfo->type)); } } 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 { WARNING_LOG("Unknown compatibility rating {} in {}", rating_str, entry->serial); } } GetStringFromObject(compatibility, "versionTested", &entry->compatibility_version_tested); GetStringFromObject(compatibility, "comments", &entry->compatibility_comments); } 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()) { WARNING_LOG("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()) { WARNING_LOG("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& libcrypt = value.find_child(to_csubstr("libcrypt")); libcrypt.valid()) { if (const std::optional libcrypt_val = StringUtil::FromChars(to_stringview(libcrypt.val())); libcrypt_val.has_value()) { entry->traits[static_cast(Trait::IsLibCryptProtected)] = true; } else { WARNING_LOG("Invalid libcrypt value in {}", entry->serial); } } 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->display_deinterlacing_mode = ParseOptionalTFromObject( settings, "displayDeinterlacingMode", &Settings::ParseDisplayDeinterlacingMode); 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"); entry->gpu_line_detect_mode = ParseOptionalTFromObject(settings, "gpuLineDetectMode", &Settings::ParseLineDetectModeName); } 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()) { WARNING_LOG("Empty disc set serial in {}", entry->serial); continue; } if (std::find(entry->disc_set_serials.begin(), entry->disc_set_serials.end(), serial_str) != entry->disc_set_serials.end()) { WARNING_LOG("Duplicate serial {} in disc set serials for {}", serial_str, entry->serial); continue; } entry->disc_set_serials.emplace_back(serial_str); } } } return true; } bool GameDatabase::ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial) { const ryml::ConstNodeRef& codes = value.find_child(to_csubstr("codes")); if (!codes.valid() || !codes.has_children()) { // use serial instead auto iter = s_code_lookup.find(serial); if (iter != s_code_lookup.end()) { WARNING_LOG("Duplicate code '{}'", serial); return false; } s_code_lookup.emplace(serial, index); return true; } u32 added = 0; for (const ryml::ConstNodeRef& current_code : codes) { const std::string_view current_code_str = to_stringview(current_code.val()); if (current_code_str.empty()) { WARNING_LOG("code is not a string in {}", serial); continue; } auto iter = s_code_lookup.find(current_code_str); if (iter != s_code_lookup.end()) { WARNING_LOG("Duplicate code '{}' in {}", current_code_str, serial); continue; } s_code_lookup.emplace(current_code_str, index); added++; } return (added > 0); } void GameDatabase::EnsureTrackHashesMapLoaded() { if (s_track_hashes_loaded) return; s_track_hashes_loaded = true; LoadTrackHashes(); } bool GameDatabase::LoadTrackHashes() { Common::Timer load_timer; std::optional gamedb_data(Host::ReadResourceFileToString(DISCDB_YAML_FILENAME, false)); if (!gamedb_data.has_value()) { ERROR_LOG("Failed to read game database"); return false; } SetRymlCallbacks(); // TODO: Parse in-place, avoid string allocations. 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 = {}; size_t serials = 0; for (const ryml::ConstNodeRef& current : root.children()) { const std::string_view serial = to_stringview(current.key()); if (serial.empty() || !current.has_children()) { WARNING_LOG("entry is not an object"); continue; } const ryml::ConstNodeRef track_data = current.find_child(to_csubstr("trackData")); if (!track_data.valid() || !track_data.has_children()) { WARNING_LOG("trackData is missing in {}", serial); continue; } u32 revision = 0; for (const ryml::ConstNodeRef& track_revisions : track_data.children()) { const ryml::ConstNodeRef tracks = track_revisions.find_child(to_csubstr("tracks")); if (!tracks.valid() || !tracks.has_children()) { WARNING_LOG("tracks member is missing in {}", serial); continue; } std::string revision_string; GetStringFromObject(track_revisions, "version", &revision_string); for (const ryml::ConstNodeRef& track : tracks) { const ryml::ConstNodeRef md5 = track.find_child("md5"); std::string_view md5_str; if (!md5.valid() || (md5_str = to_stringview(md5.val())).empty()) { WARNING_LOG("md5 is missing in track in {}", serial); continue; } const std::optional md5o = CDImageHasher::HashFromString(md5_str); if (md5o.has_value()) { 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 { WARNING_LOG("invalid md5 in {}", serial); } } revision++; } serials++; } ryml::reset_callbacks(); INFO_LOG("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() { EnsureTrackHashesMapLoaded(); return s_track_hashes_map; }