GameDatabase: Add parsing of Language field

Also speed up lookups through binary search.
This commit is contained in:
Stenzek 2024-10-12 16:22:45 +10:00
parent 2fc5856c44
commit ba0708a4ff
No known key found for this signature in database
7 changed files with 291 additions and 164 deletions

View File

@ -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<u32>(selected_entry->compatibility)].get(),
ImGui::Image(s_game_compatibility_textures[static_cast<u32>(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());

View File

@ -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<std::string_view>& lookup, const ryml::ConstNodeRef& value,
std::string_view serial);
static bool LoadTrackHashes();
static constexpr const std::array<const char*, static_cast<int>(CompatibilityRating::Count)>
@ -75,7 +75,7 @@ static constexpr const std::array<const char*, static_cast<size_t>(Compatibility
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "No Issues", "CompatibilityRating"),
}};
static constexpr const std::array<const char*, static_cast<u32>(GameDatabase::Trait::Count)> s_trait_names = {{
static constexpr const std::array<const char*, static_cast<size_t>(Trait::MaxCount)> s_trait_names = {{
"ForceInterpreter",
"ForceSoftwareRenderer",
"ForceSoftwareRendererForReadbacks",
@ -105,7 +105,7 @@ static constexpr const std::array<const char*, static_cast<u32>(GameDatabase::Tr
"IsLibCryptProtected",
}};
static constexpr const std::array<const char*, static_cast<u32>(GameDatabase::Trait::Count)> s_trait_display_names = {{
static constexpr const std::array<const char*, static_cast<size_t>(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<const char*, static_cast<u32>(GameDatabase::Tr
TRANSLATE_DISAMBIG_NOOP("GameDatabase", "Is LibCrypt Protected", "GameDatabase::Trait"),
}};
static constexpr std::array<const char*, static_cast<size_t>(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<size_t>(language)];
}
std::optional<GameDatabase::Language> GameDatabase::ParseLanguageName(std::string_view str)
{
for (size_t i = 0; i < static_cast<size_t>(Language::MaxCount); i++)
{
if (str == s_language_names[i])
return static_cast<Language>(i);
}
return std::nullopt;
}
SmallString GameDatabase::Entry::GetLanguagesString() const
{
SmallString ret;
bool first = true;
for (u32 i = 0; i < static_cast<u32>(Language::MaxCount); i++)
{
if (languages.test(i))
{
ret.append_format("{}{}", first ? "" : ", ", GetLanguageName(static_cast<Language>(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<u32>(Trait::Count); i++)
for (u32 i = 0; i < static_cast<u32>(Trait::MaxCount); i++)
{
if (traits.test(i))
ret.append_format(" - {}\n", GetTraitDisplayName(static_cast<Trait>(i)));
@ -839,8 +880,10 @@ bool GameDatabase::LoadFromCache()
{
Entry& entry = s_entries.emplace_back();
constexpr u32 num_bytes = (static_cast<u32>(Trait::Count) + 7) / 8;
std::array<u8, num_bytes> bits;
constexpr u32 trait_num_bytes = (static_cast<u32>(Trait::MaxCount) + 7) / 8;
constexpr u32 language_num_bytes = (static_cast<u32>(Language::MaxCount) + 7) / 8;
std::array<u8, trait_num_bytes> trait_bits;
std::array<u8, language_num_bytes> 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<u8>(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<GameDatabase::CompatibilityRating>(compatibility);
entry.traits.reset();
for (u32 j = 0; j < static_cast<int>(Trait::Count); j++)
for (size_t j = 0; j < static_cast<size_t>(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<size_t>(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<u8>(entry.compatibility));
constexpr u32 num_bytes = (static_cast<u32>(Trait::Count) + 7) / 8;
std::array<u8, num_bytes> bits;
bits.fill(0);
for (u32 j = 0; j < static_cast<int>(Trait::Count); j++)
constexpr u32 trait_num_bytes = (static_cast<u32>(Trait::MaxCount) + 7) / 8;
std::array<u8, trait_num_bytes> trait_bits = {};
for (size_t j = 0; j < static_cast<size_t>(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<u32>(Language::MaxCount) + 7) / 8;
std::array<u8, language_num_bytes> language_bits = {};
for (size_t j = 0; j < static_cast<size_t>(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<std::string_view> code_lookup;
for (const ryml::ConstNodeRef& current : root.cchildren())
{
// TODO: binary sort
const u32 index = static_cast<u32>(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<u32>(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<Language> planguage = ParseLanguageName(vlanguage); planguage.has_value())
entry->languages[static_cast<size_t>(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<size_t>(std::distance(s_trait_names.begin(), iter));
DebugAssert(trait_idx < static_cast<size_t>(Trait::Count));
DebugAssert(trait_idx < static_cast<size_t>(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<std::string_view>& 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++;
}

View File

@ -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 <bitset>
#include <map>
#include <string>
@ -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<static_cast<int>(Trait::Count)> traits{};
std::bitset<static_cast<size_t>(Trait::MaxCount)> traits{};
std::bitset<static_cast<size_t>(Language::MaxCount)> languages{};
std::optional<s16> display_active_start_offset;
std::optional<s16> display_active_end_offset;
std::optional<s8> display_line_start_offset;
@ -96,6 +128,10 @@ struct Entry
std::vector<std::string> disc_set_serials;
ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast<int>(trait)]; }
ALWAYS_INLINE bool HasLanguage(Language language) const { return languages.test(static_cast<size_t>(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<Language> ParseLanguageName(std::string_view str);
/// Map of track hashes for image verification
struct TrackData
{

View File

@ -28,6 +28,7 @@
#include "fmt/format.h"
#include <algorithm>
#include <bit>
#include <array>
#include <ctime>
#include <string_view>
@ -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<u32>(file.GetProgramData().size());
entry->uncompressed_size = entry->file_size;
entry->type = EntryType::PSF;
entry->compatibility = GameDatabase::CompatibilityRating::Unknown;
// Game - Title
std::optional<std::string> 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<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(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<u16>(~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(&region) || !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<u64*>(&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<u8>(DiscRegion::Count) || type >= static_cast<u8>(EntryType::Count) ||
compatibility_rating >= static_cast<u8>(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<u64*>(&ge.last_modified_time)) || !reader.ReadS8(&ge.disc_set_index) ||
region >= static_cast<u8>(DiscRegion::Count) || type >= static_cast<u8>(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<DiscRegion>(region);
ge.type = static_cast<EntryType>(type);
ge.compatibility = static_cast<GameDatabase::CompatibilityRating>(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<u8>(entry->compatibility));
return writer.IsGood();
}
@ -881,27 +845,18 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& 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<size_t>(GameDatabase::Language::English)))
{
ret = GameDatabase::GetLanguageName(
static_cast<GameDatabase::Language>(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<std::time_t>(release_date);
std::time_t date_as_time = static_cast<std::time_t>(dbentry->release_date);
#ifdef _WIN32
tm date_tm = {};
gmtime_s(&date_tm, &date_as_time);

View File

@ -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<u16>(~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;

View File

@ -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<qint64>(ge->release_date), Qt::UTC).date().year());
QDateTime::fromSecsSinceEpoch(static_cast<qint64>(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<qint64>(ge->release_date), Qt::UTC).date().year();
return ge->dbentry ? QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ge->dbentry->release_date), Qt::UTC)
.date()
.year() :
0;
case Column_Players:
return static_cast<int>(ge->max_players);
return static_cast<int>(ge->dbentry ? ge->dbentry->max_players : 0);
case Column_Region:
return static_cast<int>(ge->region);
case Column_Compatibility:
return static_cast<int>(ge->compatibility);
return static_cast<int>(ge->dbentry ? ge->dbentry->compatibility :
GameDatabase::CompatibilityRating::Unknown);
case Column_TimePlayed:
return static_cast<qlonglong>(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<u32>(ge->compatibility)];
return m_compatibility_pixmaps[static_cast<u32>(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<int>(left->compatibility) < static_cast<int>(right->compatibility));
return (static_cast<int>(left_compatibility) < static_cast<int>(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);

View File

@ -38,7 +38,7 @@ namespace GameDatabase {
enum class CompatibilityRating : u8;
}
namespace GameList {
enum class EntryType;
enum class EntryType : u8;
}
namespace QtUtils {