GameDatabase: Add parsing of Language field
Also speed up lookups through binary search.
This commit is contained in:
parent
2fc5856c44
commit
ba0708a4ff
|
@ -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());
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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(®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<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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace GameDatabase {
|
|||
enum class CompatibilityRating : u8;
|
||||
}
|
||||
namespace GameList {
|
||||
enum class EntryType;
|
||||
enum class EntryType : u8;
|
||||
}
|
||||
|
||||
namespace QtUtils {
|
||||
|
|
Loading…
Reference in New Issue