From ba0708a4ffd84d058390b94f22fe13e2aedbc48a Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 12 Oct 2024 16:22:45 +1000 Subject: [PATCH] GameDatabase: Add parsing of Language field Also speed up lookups through binary search. --- src/core/fullscreen_ui.cpp | 30 +++-- src/core/game_database.cpp | 186 ++++++++++++++++++++------- src/core/game_database.h | 45 ++++++- src/core/game_list.cpp | 88 +++++-------- src/core/game_list.h | 26 ++-- src/duckstation-qt/gamelistmodel.cpp | 78 ++++++----- src/duckstation-qt/qtutils.h | 2 +- 7 files changed, 291 insertions(+), 164 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index cb66ae66e..cc6e028ed 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -3042,10 +3042,15 @@ void FullscreenUI::DrawSummarySettingsPage() Settings::GetDiscRegionDisplayName(s_game_settings_entry->region)); } if (MenuButton(FSUI_ICONSTR(ICON_FA_STAR, "Compatibility Rating"), - GameDatabase::GetCompatibilityRatingDisplayName(s_game_settings_entry->compatibility), true)) + GameDatabase::GetCompatibilityRatingDisplayName(s_game_settings_entry->dbentry ? + s_game_settings_entry->dbentry->compatibility : + GameDatabase::CompatibilityRating::Unknown), + true)) { CopyTextToClipboard(FSUI_STR("Game compatibility rating copied to clipboard."), - GameDatabase::GetCompatibilityRatingDisplayName(s_game_settings_entry->compatibility)); + GameDatabase::GetCompatibilityRatingDisplayName( + s_game_settings_entry->dbentry ? s_game_settings_entry->dbentry->compatibility : + GameDatabase::CompatibilityRating::Unknown)); } if (MenuButton(FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Path"), s_game_settings_entry->path.c_str(), true)) { @@ -6410,14 +6415,15 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) ImGui::PushFont(g_medium_font); // developer - if (!selected_entry->developer.empty()) + if (selected_entry->dbentry && !selected_entry->dbentry->developer.empty()) { text_width = - ImGui::CalcTextSize(selected_entry->developer.c_str(), - selected_entry->developer.c_str() + selected_entry->developer.length(), false, work_width) + ImGui::CalcTextSize(selected_entry->dbentry->developer.c_str(), + selected_entry->dbentry->developer.c_str() + selected_entry->dbentry->developer.length(), + false, work_width) .x; ImGui::SetCursorPosX((work_width - text_width) / 2.0f); - ImGui::TextWrapped("%s", selected_entry->developer.c_str()); + ImGui::TextWrapped("%s", selected_entry->dbentry->developer.c_str()); } // code @@ -6438,7 +6444,8 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) } // genre - ImGui::Text(FSUI_CSTR("Genre: %s"), selected_entry->genre.c_str()); + if (selected_entry->dbentry && !selected_entry->dbentry->genre.empty()) + ImGui::Text(FSUI_CSTR("Genre: %s"), selected_entry->dbentry->genre.c_str()); // release date char release_date_str[64]; @@ -6448,13 +6455,16 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) // compatibility ImGui::TextUnformatted(FSUI_CSTR("Compatibility: ")); ImGui::SameLine(); - if (selected_entry->compatibility != GameDatabase::CompatibilityRating::Unknown) + if (selected_entry->dbentry && + selected_entry->dbentry->compatibility != GameDatabase::CompatibilityRating::Unknown) { - ImGui::Image(s_game_compatibility_textures[static_cast(selected_entry->compatibility)].get(), + ImGui::Image(s_game_compatibility_textures[static_cast(selected_entry->dbentry->compatibility)].get(), LayoutScale(64.0f, 16.0f)); ImGui::SameLine(); } - ImGui::Text(" (%s)", GameDatabase::GetCompatibilityRatingDisplayName(selected_entry->compatibility)); + ImGui::Text(" (%s)", GameDatabase::GetCompatibilityRatingDisplayName( + selected_entry->dbentry ? selected_entry->dbentry->compatibility : + GameDatabase::CompatibilityRating::Unknown)); // play time ImGui::Text(FSUI_CSTR("Time Played: %s"), GameList::FormatTimespan(selected_entry->total_played_time).c_str()); diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp index 80026b16c..2bfa4ca1b 100644 --- a/src/core/game_database.cpp +++ b/src/core/game_database.cpp @@ -40,10 +40,9 @@ namespace GameDatabase { enum : u32 { GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48, - GAME_DATABASE_CACHE_VERSION = 16, + GAME_DATABASE_CACHE_VERSION = 17, }; -static Entry* GetMutableEntry(std::string_view serial); static const Entry* GetEntryForId(std::string_view code); static bool LoadFromCache(); @@ -52,7 +51,8 @@ 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 ParseYamlCodes(PreferUnorderedStringMap& lookup, const ryml::ConstNodeRef& value, + std::string_view serial); static bool LoadTrackHashes(); static constexpr const std::array(CompatibilityRating::Count)> @@ -75,7 +75,7 @@ static constexpr const std::array(Compatibility TRANSLATE_DISAMBIG_NOOP("GameDatabase", "No Issues", "CompatibilityRating"), }}; -static constexpr const std::array(GameDatabase::Trait::Count)> s_trait_names = {{ +static constexpr const std::array(Trait::MaxCount)> s_trait_names = {{ "ForceInterpreter", "ForceSoftwareRenderer", "ForceSoftwareRendererForReadbacks", @@ -105,7 +105,7 @@ static constexpr const std::array(GameDatabase::Tr "IsLibCryptProtected", }}; -static constexpr const std::array(GameDatabase::Trait::Count)> s_trait_display_names = {{ +static constexpr const std::array(Trait::MaxCount)> s_trait_display_names = {{ TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Interpreter", "GameDatabase::Trait"), TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Software Renderer", "GameDatabase::Trait"), TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Force Software Renderer For Readbacks", "GameDatabase::Trait"), @@ -135,6 +135,12 @@ static constexpr const std::array(GameDatabase::Tr TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Is LibCrypt Protected", "GameDatabase::Trait"), }}; +static constexpr std::array(Language::MaxCount)> s_language_names = {{ + "Catalan", "Chinese", "Czech", "Danish", "Dutch", "English", "Finnish", + "French", "German", "Greek", "Hebrew", "Iranian", "Italian", "Japanese", + "Korean", "Norwegian", "Polish", "Portuguese", "Russian", "Spanish", "Swedish", +}}; + static constexpr const char* GAMEDB_YAML_FILENAME = "gamedb.yaml"; static constexpr const char* DISCDB_YAML_FILENAME = "discdb.yaml"; @@ -245,20 +251,15 @@ const GameDatabase::Entry* GameDatabase::GetEntryForGameDetails(const std::strin const GameDatabase::Entry* GameDatabase::GetEntryForSerial(std::string_view serial) { + if (serial.empty()) + return nullptr; + 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 auto it = + std::lower_bound(s_entries.cbegin(), s_entries.cend(), serial, + [](const Entry& entry, const std::string_view& search) { return (entry.serial < search); }); + return (it != s_entries.end() && it->serial == serial) ? &(*it) : nullptr; } const char* GameDatabase::GetTraitName(Trait trait) @@ -283,6 +284,42 @@ const char* GameDatabase::GetCompatibilityRatingDisplayName(CompatibilityRating ""; } +const char* GameDatabase::GetLanguageName(Language language) +{ + return s_language_names[static_cast(language)]; +} + +std::optional GameDatabase::ParseLanguageName(std::string_view str) +{ + for (size_t i = 0; i < static_cast(Language::MaxCount); i++) + { + if (str == s_language_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + +SmallString GameDatabase::Entry::GetLanguagesString() const +{ + SmallString ret; + + bool first = true; + for (u32 i = 0; i < static_cast(Language::MaxCount); i++) + { + if (languages.test(i)) + { + ret.append_format("{}{}", first ? "" : ", ", GetLanguageName(static_cast(i))); + first = false; + } + } + + if (ret.empty()) + ret.append(TRANSLATE_SV("GameDatabase", "Unknown")); + + return ret; +} + void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_messages) const { if (display_active_start_offset.has_value()) @@ -732,6 +769,10 @@ 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); + + if (languages.any()) + ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Languages"), GetLanguagesString()); + ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Rating"), GetCompatibilityRatingDisplayName(compatibility)); @@ -759,7 +800,7 @@ std::string GameDatabase::Entry::GenerateCompatibilityReport() const if (traits.any()) { ret.append_format("**{}**\n\n", TRANSLATE_SV("GameDatabase", "Traits")); - for (u32 i = 0; i < static_cast(Trait::Count); i++) + for (u32 i = 0; i < static_cast(Trait::MaxCount); i++) { if (traits.test(i)) ret.append_format(" - {}\n", GetTraitDisplayName(static_cast(i))); @@ -839,8 +880,10 @@ bool GameDatabase::LoadFromCache() { Entry& entry = s_entries.emplace_back(); - constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8; - std::array bits; + constexpr u32 trait_num_bytes = (static_cast(Trait::MaxCount) + 7) / 8; + constexpr u32 language_num_bytes = (static_cast(Language::MaxCount) + 7) / 8; + std::array trait_bits; + std::array language_bits; u8 compatibility; u32 num_disc_set_serials; @@ -852,7 +895,8 @@ bool GameDatabase::LoadFromCache() !reader.ReadU8(&entry.min_players) || !reader.ReadU8(&entry.max_players) || !reader.ReadU8(&entry.min_blocks) || !reader.ReadU8(&entry.max_blocks) || !reader.ReadU16(&entry.supported_controllers) || !reader.ReadU8(&compatibility) || compatibility >= static_cast(GameDatabase::CompatibilityRating::Count) || - !reader.Read(bits.data(), num_bytes) || !reader.ReadOptionalT(&entry.display_active_start_offset) || + !reader.Read(trait_bits.data(), trait_num_bytes) || !reader.Read(language_bits.data(), language_num_bytes) || + !reader.ReadOptionalT(&entry.display_active_start_offset) || !reader.ReadOptionalT(&entry.display_active_end_offset) || !reader.ReadOptionalT(&entry.display_line_start_offset) || !reader.ReadOptionalT(&entry.display_line_end_offset) || !reader.ReadOptionalT(&entry.display_crop_mode) || @@ -881,11 +925,16 @@ bool GameDatabase::LoadFromCache() entry.compatibility = static_cast(compatibility); entry.traits.reset(); - for (u32 j = 0; j < static_cast(Trait::Count); j++) + for (size_t j = 0; j < static_cast(Trait::MaxCount); j++) { - if ((bits[j / 8] & (1u << (j % 8))) != 0) + if ((trait_bits[j / 8] & (1u << (j % 8))) != 0) entry.traits[j] = true; } + for (size_t j = 0; j < static_cast(Language::MaxCount); j++) + { + if ((language_bits[j / 8] & (1u << (j % 8))) != 0) + entry.languages[j] = true; + } } for (u32 i = 0; i < num_codes; i++) @@ -941,16 +990,24 @@ bool GameDatabase::SaveToCache() writer.WriteU16(entry.supported_controllers); writer.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++) + constexpr u32 trait_num_bytes = (static_cast(Trait::MaxCount) + 7) / 8; + std::array trait_bits = {}; + for (size_t j = 0; j < static_cast(Trait::MaxCount); j++) { if (entry.traits[j]) - bits[j / 8] |= (1u << (j % 8)); + trait_bits[j / 8] |= (1u << (j % 8)); } - writer.Write(bits.data(), num_bytes); + writer.Write(trait_bits.data(), trait_num_bytes); + + constexpr u32 language_num_bytes = (static_cast(Language::MaxCount) + 7) / 8; + std::array language_bits = {}; + for (size_t j = 0; j < static_cast(Language::MaxCount); j++) + { + if (entry.languages[j]) + language_bits[j / 8] |= (1u << (j % 8)); + } + writer.Write(language_bits.data(), language_num_bytes); writer.WriteOptionalT(entry.display_active_start_offset); writer.WriteOptionalT(entry.display_active_end_offset); @@ -1007,33 +1064,55 @@ bool GameDatabase::LoadGameDBYaml() const ryml::ConstNodeRef root = tree.rootref(); s_entries.reserve(root.num_children()); + PreferUnorderedStringMap code_lookup; + for (const ryml::ConstNodeRef& current : root.cchildren()) { - // TODO: binary sort - const u32 index = static_cast(s_entries.size()); + const std::string_view serial = to_stringview(current.key()); + if (current.empty()) + { + ERROR_LOG("Missing serial for entry."); + return false; + } + Entry& entry = s_entries.emplace_back(); + entry.serial = serial; if (!ParseYamlEntry(&entry, current)) { s_entries.pop_back(); continue; } - ParseYamlCodes(index, current, entry.serial); + ParseYamlCodes(code_lookup, current, serial); } + // Sorting must be done before generating code lookup, because otherwise the indices won't match. + s_entries.shrink_to_fit(); + std::sort(s_entries.begin(), s_entries.end(), + [](const Entry& lhs, const Entry& rhs) { return (lhs.serial < rhs.serial); }); + ryml::reset_callbacks(); + + for (const auto& [code, serial] : code_lookup) + { + const auto it = + std::lower_bound(s_entries.cbegin(), s_entries.cend(), serial, + [](const Entry& entry, const std::string_view& search) { return (entry.serial < search); }); + if (it == s_entries.end() || it->serial != serial) + { + ERROR_LOG("Somehow we messed up our code lookup for {} and {}?!", code, serial); + continue; + } + + if (!s_code_lookup.emplace(code, static_cast(std::distance(s_entries.cbegin(), it))).second) + ERROR_LOG("Failed to insert code {}", code); + } + 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()) @@ -1047,6 +1126,18 @@ bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value) GetUIntFromObject(metadata, "minBlocks", &entry->min_blocks); GetUIntFromObject(metadata, "maxBlocks", &entry->max_blocks); + if (const ryml::ConstNodeRef languages = metadata.find_child(to_csubstr("languages")); languages.valid()) + { + for (const ryml::ConstNodeRef language : languages.cchildren()) + { + const std::string_view vlanguage = to_stringview(language.val()); + if (const std::optional planguage = ParseLanguageName(vlanguage); planguage.has_value()) + entry->languages[static_cast(planguage.value())] = true; + else + WARNING_LOG("Unknown language {} in {}.", vlanguage, entry->serial); + } + } + entry->release_date = 0; { std::string release_date; @@ -1144,7 +1235,7 @@ bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value) } const size_t trait_idx = static_cast(std::distance(s_trait_names.begin(), iter)); - DebugAssert(trait_idx < static_cast(Trait::Count)); + DebugAssert(trait_idx < static_cast(Trait::MaxCount)); entry->traits[trait_idx] = true; } } @@ -1215,20 +1306,21 @@ bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value) return true; } -bool GameDatabase::ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, std::string_view serial) +bool GameDatabase::ParseYamlCodes(PreferUnorderedStringMap& lookup, 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()) + auto iter = lookup.find(serial); + if (iter != lookup.end()) { WARNING_LOG("Duplicate code '{}'", serial); return false; } - s_code_lookup.emplace(serial, index); + lookup.emplace(serial, serial); return true; } @@ -1242,14 +1334,14 @@ bool GameDatabase::ParseYamlCodes(u32 index, const ryml::ConstNodeRef& value, st continue; } - auto iter = s_code_lookup.find(current_code_str); - if (iter != s_code_lookup.end()) + auto iter = lookup.find(current_code_str); + if (iter != lookup.end()) { WARNING_LOG("Duplicate code '{}' in {}", current_code_str, serial); continue; } - s_code_lookup.emplace(current_code_str, index); + lookup.emplace(current_code_str, serial); added++; } diff --git a/src/core/game_database.h b/src/core/game_database.h index 2c1945c30..958ab92e1 100644 --- a/src/core/game_database.h +++ b/src/core/game_database.h @@ -2,8 +2,13 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once -#include "core/types.h" + +#include "types.h" + #include "util/cd_image_hasher.h" + +#include "common/small_string.h" + #include #include #include @@ -56,7 +61,33 @@ enum class Trait : u32 ForceCDROMSubQSkew, IsLibCryptProtected, - Count + MaxCount +}; + +enum class Language : u8 +{ + Catalan, + Chinese, + Czech, + Danish, + Dutch, + English, + Finnish, + French, + German, + Greek, + Hebrew, + Iranian, + Italian, + Japanese, + Korean, + Norwegian, + Polish, + Portuguese, + Russian, + Spanish, + Swedish, + MaxCount, }; struct Entry @@ -77,7 +108,8 @@ struct Entry u16 supported_controllers; CompatibilityRating compatibility; - std::bitset(Trait::Count)> traits{}; + std::bitset(Trait::MaxCount)> traits{}; + std::bitset(Language::MaxCount)> languages{}; std::optional display_active_start_offset; std::optional display_active_end_offset; std::optional display_line_start_offset; @@ -96,6 +128,10 @@ struct Entry std::vector disc_set_serials; ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast(trait)]; } + ALWAYS_INLINE bool HasLanguage(Language language) const { return languages.test(static_cast(language)); } + ALWAYS_INLINE bool HasAnyLanguage() const { return !languages.none(); } + + SmallString GetLanguagesString() const; void ApplySettings(Settings& settings, bool display_osd_messages) const; @@ -117,6 +153,9 @@ const char* GetTraitDisplayName(Trait trait); const char* GetCompatibilityRatingName(CompatibilityRating rating); const char* GetCompatibilityRatingDisplayName(CompatibilityRating rating); +const char* GetLanguageName(Language language); +std::optional ParseLanguageName(std::string_view str); + /// Map of track hashes for image verification struct TrackData { diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 0b22c9f72..090e802b9 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -28,6 +28,7 @@ #include "fmt/format.h" #include +#include #include #include #include @@ -47,7 +48,7 @@ namespace { enum : u32 { GAME_LIST_CACHE_SIGNATURE = 0x45434C48, - GAME_LIST_CACHE_VERSION = 35, + GAME_LIST_CACHE_VERSION = 36, PLAYED_TIME_SERIAL_LENGTH = 32, PLAYED_TIME_LAST_TIME_LENGTH = 20, // uint64 @@ -200,7 +201,6 @@ bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry) entry->file_size = ZeroExtend64(file_size); entry->uncompressed_size = entry->file_size; entry->type = EntryType::PSExe; - entry->compatibility = GameDatabase::CompatibilityRating::Unknown; return true; } @@ -217,7 +217,6 @@ bool GameList::GetPsfListEntry(const std::string& path, Entry* entry) entry->file_size = static_cast(file.GetProgramData().size()); entry->uncompressed_size = entry->file_size; entry->type = EntryType::PSF; - entry->compatibility = GameDatabase::CompatibilityRating::Unknown; // Game - Title std::optional game(file.GetTagString("game")); @@ -255,7 +254,6 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry) entry->file_size = cdi->GetSizeOnDisk(); entry->uncompressed_size = static_cast(CDImage::RAW_SECTOR_SIZE) * static_cast(cdi->GetLBACount()); entry->type = EntryType::Disc; - entry->compatibility = GameDatabase::CompatibilityRating::Unknown; std::string id; System::GetGameDetailsFromImage(cdi.get(), &id, &entry->hash); @@ -267,16 +265,7 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry) // pull from database entry->serial = dentry->serial; entry->title = dentry->title; - entry->genre = dentry->genre; - entry->publisher = dentry->publisher; - entry->developer = dentry->developer; - entry->release_date = dentry->release_date; - entry->min_players = dentry->min_players; - entry->max_players = dentry->max_players; - entry->min_blocks = dentry->min_blocks; - entry->max_blocks = dentry->max_blocks; - entry->supported_controllers = dentry->supported_controllers; - entry->compatibility = dentry->compatibility; + entry->dbentry = dentry; if (!cdi->HasSubImages()) { @@ -293,18 +282,9 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry) } else { - const std::string display_name(FileSystem::GetDisplayNameFromPath(path)); - // no game code, so use the filename title entry->serial = std::move(id); - entry->title = Path::GetFileTitle(display_name); - entry->compatibility = GameDatabase::CompatibilityRating::Unknown; - entry->release_date = 0; - entry->min_players = 0; - entry->max_players = 0; - entry->min_blocks = 0; - entry->max_blocks = 0; - entry->supported_controllers = static_cast(~0u); + entry->title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path)); } // region detection @@ -352,6 +332,7 @@ bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry, return false; *entry = std::move(iter->second); + entry->dbentry = GameDatabase::GetEntryForSerial(entry->serial); s_cache_map.erase(iter); ApplyCustomAttributes(path, entry, custom_attributes_ini); return true; @@ -374,19 +355,13 @@ bool GameList::LoadEntriesFromCache(BinaryFileReader& reader) u8 type; u8 region; - u8 compatibility_rating; if (!reader.ReadU8(&type) || !reader.ReadU8(®ion) || !reader.ReadSizePrefixedString(&path) || !reader.ReadSizePrefixedString(&ge.serial) || !reader.ReadSizePrefixedString(&ge.title) || - !reader.ReadSizePrefixedString(&ge.disc_set_name) || !reader.ReadSizePrefixedString(&ge.genre) || - !reader.ReadSizePrefixedString(&ge.publisher) || !reader.ReadSizePrefixedString(&ge.developer) || - !reader.ReadU64(&ge.hash) || !reader.ReadS64(&ge.file_size) || !reader.ReadU64(&ge.uncompressed_size) || - !reader.ReadU64(reinterpret_cast(&ge.last_modified_time)) || !reader.ReadU64(&ge.release_date) || - !reader.ReadU16(&ge.supported_controllers) || !reader.ReadU8(&ge.min_players) || - !reader.ReadU8(&ge.max_players) || !reader.ReadU8(&ge.min_blocks) || !reader.ReadU8(&ge.max_blocks) || - !reader.ReadS8(&ge.disc_set_index) || !reader.ReadU8(&compatibility_rating) || - region >= static_cast(DiscRegion::Count) || type >= static_cast(EntryType::Count) || - compatibility_rating >= static_cast(GameDatabase::CompatibilityRating::Count)) + !reader.ReadSizePrefixedString(&ge.disc_set_name) || !reader.ReadU64(&ge.hash) || + !reader.ReadS64(&ge.file_size) || !reader.ReadU64(&ge.uncompressed_size) || + !reader.ReadU64(reinterpret_cast(&ge.last_modified_time)) || !reader.ReadS8(&ge.disc_set_index) || + region >= static_cast(DiscRegion::Count) || type >= static_cast(EntryType::Count)) { WARNING_LOG("Game list cache entry is corrupted"); return false; @@ -395,7 +370,6 @@ bool GameList::LoadEntriesFromCache(BinaryFileReader& reader) ge.path = path; ge.region = static_cast(region); ge.type = static_cast(type); - ge.compatibility = static_cast(compatibility_rating); auto iter = s_cache_map.find(ge.path); if (iter != s_cache_map.end()) @@ -415,21 +389,11 @@ bool GameList::WriteEntryToCache(const Entry* entry, BinaryFileWriter& writer) writer.WriteSizePrefixedString(entry->serial); writer.WriteSizePrefixedString(entry->title); writer.WriteSizePrefixedString(entry->disc_set_name); - writer.WriteSizePrefixedString(entry->genre); - writer.WriteSizePrefixedString(entry->publisher); - writer.WriteSizePrefixedString(entry->developer); writer.WriteU64(entry->hash); writer.WriteS64(entry->file_size); writer.WriteU64(entry->uncompressed_size); writer.WriteU64(entry->last_modified_time); - writer.WriteU64(entry->release_date); - writer.WriteU16(entry->supported_controllers); - writer.WriteU8(entry->min_players); - writer.WriteU8(entry->max_players); - writer.WriteU8(entry->min_blocks); - writer.WriteU8(entry->max_blocks); writer.WriteS8(entry->disc_set_index); - writer.WriteU8(static_cast(entry->compatibility)); return writer.IsGood(); } @@ -881,27 +845,18 @@ void GameList::CreateDiscSetEntries(const std::vector& excluded_pat } Entry set_entry; + set_entry.dbentry = entry.dbentry; set_entry.type = EntryType::DiscSet; set_entry.region = entry.region; set_entry.path = disc_set_name; set_entry.serial = entry.serial; set_entry.title = entry.disc_set_name; - set_entry.genre = entry.developer; - set_entry.publisher = entry.publisher; - set_entry.developer = entry.developer; set_entry.hash = entry.hash; set_entry.file_size = 0; set_entry.uncompressed_size = 0; set_entry.last_modified_time = entry.last_modified_time; set_entry.last_played_time = 0; set_entry.total_played_time = 0; - set_entry.release_date = entry.release_date; - set_entry.supported_controllers = entry.supported_controllers; - set_entry.min_players = entry.min_players; - set_entry.max_players = entry.max_players; - set_entry.min_blocks = entry.min_blocks; - set_entry.max_blocks = entry.max_blocks; - set_entry.compatibility = entry.compatibility; // figure out play time for all discs, and sum it // we do this via lookups, rather than the other entries, because of duplicates @@ -1016,12 +971,31 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha return Path::Combine(EmuFolders::Covers, Path::SanitizeFileName(name)); } +std::string_view GameList::Entry::GetLanguageIcon() const +{ + // If there's only one language, this is the flag we want to use. + // Except if it's English, then we want to use the disc region's flag. + std::string_view ret; + if (dbentry && dbentry->languages.count() == 1 && + !dbentry->languages.test(static_cast(GameDatabase::Language::English))) + { + ret = GameDatabase::GetLanguageName( + static_cast(std::countr_zero(dbentry->languages.to_ulong()))); + } + else + { + ret = Settings::GetDiscRegionName(region); + } + + return ret; +} + size_t GameList::Entry::GetReleaseDateString(char* buffer, size_t buffer_size) const { - if (release_date == 0) + if (!dbentry || dbentry->release_date == 0) return StringUtil::Strlcpy(buffer, "Unknown", buffer_size); - std::time_t date_as_time = static_cast(release_date); + std::time_t date_as_time = static_cast(dbentry->release_date); #ifdef _WIN32 tm date_tm = {}; gmtime_s(&date_tm, &date_as_time); diff --git a/src/core/game_list.h b/src/core/game_list.h index ada9150d7..8a587b8aa 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -22,7 +22,7 @@ class ProgressCallback; struct SystemBootParameters; namespace GameList { -enum class EntryType +enum class EntryType : u8 { Disc, DiscSet, @@ -37,13 +37,18 @@ struct Entry EntryType type = EntryType::Disc; DiscRegion region = DiscRegion::Other; + s8 disc_set_index = -1; + bool disc_set_member = false; + bool has_custom_title = false; + bool has_custom_region = false; + std::string path; std::string serial; std::string title; std::string disc_set_name; - std::string genre; - std::string publisher; - std::string developer; + + const GameDatabase::Entry* dbentry = nullptr; + u64 hash = 0; s64 file_size = 0; u64 uncompressed_size = 0; @@ -51,18 +56,7 @@ struct Entry std::time_t last_played_time = 0; std::time_t total_played_time = 0; - u64 release_date = 0; - u16 supported_controllers = static_cast(~0u); - u8 min_players = 1; - u8 max_players = 1; - u8 min_blocks = 0; - u8 max_blocks = 0; - s8 disc_set_index = -1; - bool disc_set_member = false; - bool has_custom_title = false; - bool has_custom_region = false; - - GameDatabase::CompatibilityRating compatibility = GameDatabase::CompatibilityRating::Unknown; + std::string_view GetLanguageIcon() const; size_t GetReleaseDateString(char* buffer, size_t buffer_size) const; diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index 6b78ff536..67e3b31fa 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -409,20 +409,22 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList: return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path)); case Column_Developer: - return QString::fromStdString(ge->developer); + return (ge->dbentry && !ge->dbentry->developer.empty()) ? QString::fromStdString(ge->dbentry->developer) : + QString(); case Column_Publisher: - return QString::fromStdString(ge->publisher); + return (ge->dbentry && !ge->dbentry->publisher.empty()) ? QString::fromStdString(ge->dbentry->publisher) : + QString(); case Column_Genre: - return QString::fromStdString(ge->genre); + return (ge->dbentry && !ge->dbentry->genre.empty()) ? QString::fromStdString(ge->dbentry->genre) : QString(); case Column_Year: { - if (ge->release_date != 0) + if (ge->dbentry && ge->dbentry->release_date != 0) { return QStringLiteral("%1").arg( - QDateTime::fromSecsSinceEpoch(static_cast(ge->release_date), Qt::UTC).date().year()); + QDateTime::fromSecsSinceEpoch(static_cast(ge->dbentry->release_date), Qt::UTC).date().year()); } else { @@ -432,10 +434,10 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList: case Column_Players: { - if (ge->min_players == ge->max_players) - return QStringLiteral("%1").arg(ge->min_players); + if (ge->dbentry->min_players == ge->dbentry->max_players) + return QStringLiteral("%1").arg(ge->dbentry->min_players); else - return QStringLiteral("%1-%2").arg(ge->min_players).arg(ge->max_players); + return QStringLiteral("%1-%2").arg(ge->dbentry->min_players).arg(ge->dbentry->max_players); } case Column_FileSize: @@ -488,25 +490,31 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList: return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path)); case Column_Developer: - return QString::fromStdString(ge->developer); + return (ge->dbentry && !ge->dbentry->developer.empty()) ? QString::fromStdString(ge->dbentry->developer) : + QString(); case Column_Publisher: - return QString::fromStdString(ge->publisher); + return (ge->dbentry && !ge->dbentry->publisher.empty()) ? QString::fromStdString(ge->dbentry->publisher) : + QString(); case Column_Genre: - return QString::fromStdString(ge->genre); + return (ge->dbentry && !ge->dbentry->genre.empty()) ? QString::fromStdString(ge->dbentry->genre) : QString(); case Column_Year: - return QDateTime::fromSecsSinceEpoch(static_cast(ge->release_date), Qt::UTC).date().year(); + return ge->dbentry ? QDateTime::fromSecsSinceEpoch(static_cast(ge->dbentry->release_date), Qt::UTC) + .date() + .year() : + 0; case Column_Players: - return static_cast(ge->max_players); + return static_cast(ge->dbentry ? ge->dbentry->max_players : 0); case Column_Region: return static_cast(ge->region); case Column_Compatibility: - return static_cast(ge->compatibility); + return static_cast(ge->dbentry ? ge->dbentry->compatibility : + GameDatabase::CompatibilityRating::Unknown); case Column_TimePlayed: return static_cast(ge->total_played_time); @@ -541,7 +549,8 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList: case Column_Compatibility: { - return m_compatibility_pixmaps[static_cast(ge->compatibility)]; + return m_compatibility_pixmaps[static_cast(ge->dbentry ? ge->dbentry->compatibility : + GameDatabase::CompatibilityRating::Unknown)]; } case Column_Cover: @@ -684,10 +693,14 @@ bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry* case Column_Compatibility: { - if (left->compatibility == right->compatibility) + const GameDatabase::CompatibilityRating left_compatibility = + left->dbentry ? left->dbentry->compatibility : GameDatabase::CompatibilityRating::Unknown; + const GameDatabase::CompatibilityRating right_compatibility = + right->dbentry ? right->dbentry->compatibility : GameDatabase::CompatibilityRating::Unknown; + if (left_compatibility == right_compatibility) return titlesLessThan(left, right); - return (static_cast(left->compatibility) < static_cast(right->compatibility)); + return (static_cast(left_compatibility) < static_cast(right_compatibility)); } case Column_FileSize: @@ -708,31 +721,36 @@ bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry* case Column_Genre: { - if (left->genre == right->genre) - return titlesLessThan(left, right); - return (StringUtil::Strcasecmp(left->genre.c_str(), right->genre.c_str()) < 0); + const int compres = + StringUtil::CompareNoCase(left->dbentry ? std::string_view(left->dbentry->genre) : std::string_view(), + right->dbentry ? std::string_view(right->dbentry->genre) : std::string_view()); + return (compres == 0) ? titlesLessThan(left, right) : (compres < 0); } case Column_Developer: { - if (left->developer == right->developer) - return titlesLessThan(left, right); - return (StringUtil::Strcasecmp(left->developer.c_str(), right->developer.c_str()) < 0); + const int compres = + StringUtil::CompareNoCase(left->dbentry ? std::string_view(left->dbentry->developer) : std::string_view(), + right->dbentry ? std::string_view(right->dbentry->developer) : std::string_view()); + return (compres == 0) ? titlesLessThan(left, right) : (compres < 0); } case Column_Publisher: { - if (left->publisher == right->publisher) - return titlesLessThan(left, right); - return (StringUtil::Strcasecmp(left->publisher.c_str(), right->publisher.c_str()) < 0); + const int compres = + StringUtil::CompareNoCase(left->dbentry ? std::string_view(left->dbentry->publisher) : std::string_view(), + right->dbentry ? std::string_view(right->dbentry->publisher) : std::string_view()); + return (compres == 0) ? titlesLessThan(left, right) : (compres < 0); } case Column_Year: { - if (left->release_date == right->release_date) + const u64 ldate = left->dbentry ? left->dbentry->release_date : 0; + const u64 rdate = right->dbentry ? right->dbentry->release_date : 0; + if (ldate == rdate) return titlesLessThan(left, right); - return (left->release_date < right->release_date); + return (ldate < rdate); } case Column_TimePlayed: @@ -753,8 +771,8 @@ bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry* case Column_Players: { - u8 left_players = (left->min_players << 4) + left->max_players; - u8 right_players = (right->min_players << 4) + right->max_players; + const u8 left_players = left->dbentry ? ((left->dbentry->min_players << 4) + left->dbentry->max_players) : 0; + const u8 right_players = right->dbentry ? ((right->dbentry->min_players << 4) + right->dbentry->max_players) : 0; if (left_players == right_players) return titlesLessThan(left, right); diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index cdd9ebe6d..310f69d28 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -38,7 +38,7 @@ namespace GameDatabase { enum class CompatibilityRating : u8; } namespace GameList { -enum class EntryType; +enum class EntryType : u8; } namespace QtUtils {