GameDatabase: Refactor disc sets

Store the sets in a separate file, and use pointers to reference instead
of string matching. This allows us to use the unsuffixed display names.
pull/3539/head^2
Stenzek 1 year ago
parent d00b897257
commit 8bfc31a59c
No known key found for this signature in database

@ -33,6 +33,10 @@ jobs:
shell: bash
run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/gamedb.yaml
- name: Check Disc Sets
shell: bash
run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/discsets.yaml
- name: Check DiscDB
shell: bash
run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/discdb.yaml

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -428,9 +428,9 @@ void Cheats::EnumerateChtFiles(const std::string_view serial, std::optional<Game
{
// Is this game part of a disc set? Try codes for the other discs.
const GameDatabase::Entry* gentry = GameDatabase::GetEntryForSerial(serial);
if (gentry && gentry->disc_set_serials.size() > 1)
if (gentry && gentry->disc_set)
{
for (const std::string_view& set_serial : gentry->disc_set_serials)
for (const std::string_view& set_serial : gentry->disc_set->serials)
{
if (set_serial == serial)
continue;

@ -419,12 +419,13 @@ static void DrawResumeStateSelector();
//////////////////////////////////////////////////////////////////////////
// Game List
//////////////////////////////////////////////////////////////////////////
static bool ShouldShowLocalizedTitles();
static void DrawGameListWindow();
static void DrawGameList(const ImVec2& heading_size);
static void DrawGameGrid(const ImVec2& heading_size);
static void HandleGameListActivate(const GameList::Entry* entry);
static void HandleGameListOptions(const GameList::Entry* entry);
static void HandleSelectDiscForDiscSet(std::string_view disc_set_name);
static void HandleSelectDiscForDiscSet(const GameDatabase::DiscSetEntry* dsentry);
static void DrawGameListSettingsPage();
static void SwitchToGameList();
static void PopulateGameListEntryList();
@ -1682,10 +1683,10 @@ void FullscreenUI::BeginChangeDiscOnCPUThread(bool needs_pause)
return;
}
if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && !entry->disc_set_serials.empty())
if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && entry->disc_set)
{
const auto lock = GameList::GetLock();
auto matches = GameList::GetMatchingEntriesForSerial(entry->disc_set_serials);
auto matches = GameList::GetEntriesInDiscSet(entry->disc_set, ShouldShowLocalizedTitles());
if (matches.size() > 1)
{
options.reserve(matches.size() + 1);
@ -4078,11 +4079,11 @@ void FullscreenUI::DrawSummarySettingsPage()
MenuHeading(FSUI_VSTR("Options"));
if (s_state.game_settings_db_entry && !s_state.game_settings_db_entry->disc_set_serials.empty())
if (s_state.game_settings_db_entry && s_state.game_settings_db_entry->disc_set)
{
// only enable for first disc
const bool is_first_disc =
(s_state.game_settings_db_entry->serial == s_state.game_settings_db_entry->disc_set_serials.front());
(s_state.game_settings_db_entry->serial == s_state.game_settings_db_entry->disc_set->serials.front());
DrawToggleSetting(
GetEditingSettingsInterface(), FSUI_ICONVSTR(ICON_FA_COMPACT_DISC, "Use Separate Disc Settings"),
FSUI_VSTR(
@ -7878,6 +7879,11 @@ void FullscreenUI::PopulateGameListEntryList()
});
}
bool FullscreenUI::ShouldShowLocalizedTitles()
{
return Host::GetBaseBoolSettingValue("Main", "FullscreenUIShowLocalizedTitles", true);
}
void FullscreenUI::DrawGameListWindow()
{
auto game_list_lock = GameList::GetLock();
@ -7993,6 +7999,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
if (!AreAnyDialogsOpen() && WantsToCloseMenu())
BeginTransition([]() { SwitchToMainWindow(MainWindowType::Landing); });
const bool localized_titles = ShouldShowLocalizedTitles();
auto game_list_lock = GameList::GetLock();
const GameList::Entry* selected_entry = nullptr;
PopulateGameListEntryList();
@ -8026,7 +8033,8 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
summary.format("{} | {} | {} MB", entry->serial, Path::GetFileName(entry->path), to_mb(entry->file_size));
}
const ImGuiFullscreen::MenuButtonBounds mbb(entry->GetDisplayTitle(), {}, summary, row_left_margin);
const ImGuiFullscreen::MenuButtonBounds mbb(entry->GetDisplayTitle(localized_titles), {}, summary,
row_left_margin);
bool visible, hovered;
bool pressed = MenuButtonFrame(entry->path, true, mbb.frame_bb, &visible, &hovered);
@ -8042,7 +8050,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
ImGui::GetWindowDrawList()->AddImage(cover_texture, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
RenderShadowedTextClipped(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.BoldFontWeight, mbb.title_bb.Min,
mbb.title_bb.Max, text_color, entry->GetDisplayTitle(), &mbb.title_size,
mbb.title_bb.Max, text_color, entry->GetDisplayTitle(localized_titles), &mbb.title_size,
ImVec2(0.0f, 0.0f), mbb.title_size.x, &mbb.title_bb);
if (!summary.empty())
@ -8145,7 +8153,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
if (selected_entry)
{
const ImVec4 subtitle_text_color = DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]);
const std::string_view title = selected_entry->GetDisplayTitle();
const std::string_view title = selected_entry->GetDisplayTitle(localized_titles);
// title
ImGui::PushFont(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.BoldFontWeight);
@ -8348,6 +8356,7 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
const ImGuiStyle& style = ImGui::GetStyle();
const bool localized_titles = ShouldShowLocalizedTitles();
const float title_font_size = UIStyle.MediumFontSize;
const float title_font_weight = UIStyle.BoldFontWeight;
const float avail_width = ImGui::GetContentRegionAvail().x;
@ -8389,7 +8398,7 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
for (size_t row_entry_index = entry_index; row_entry_index < row_entry_index_end; row_entry_index++)
{
const GameList::Entry* row_entry = s_state.game_list_sorted_entries[row_entry_index];
const std::string_view row_title = row_entry->GetDisplayTitle();
const std::string_view row_title = row_entry->GetDisplayTitle(localized_titles);
const ImVec2 this_title_size = UIStyle.Font->CalcTextSizeA(title_font_size, title_font_weight, image_width,
image_width, IMSTR_START_END(row_title));
row_item_height = std::max(row_item_height, this_title_size.y);
@ -8401,7 +8410,7 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
ImVec2 title_size;
if (s_state.game_grid_show_titles)
{
const std::string_view title = entry->GetDisplayTitle();
const std::string_view title = entry->GetDisplayTitle(localized_titles);
title_size = UIStyle.Font->CalcTextSizeA(title_font_size, title_font_weight, image_width, image_width,
IMSTR_START_END(title));
}
@ -8453,7 +8462,7 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
const ImRect title_bb(ImVec2(bb.Min.x, bb.Min.y + image_height + title_spacing), bb.Max);
ImGuiFullscreen::RenderMultiLineShadowedTextClipped(
dl, UIStyle.Font, title_font_size, title_font_weight, title_bb.Min, title_bb.Max, text_color,
entry->GetDisplayTitle(), LAYOUT_CENTER_ALIGN_TEXT, image_width, &title_bb);
entry->GetDisplayTitle(localized_titles), LAYOUT_CENTER_ALIGN_TEXT, image_width, &title_bb);
}
if (pressed)
@ -8494,7 +8503,7 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
{
if (entry->IsDiscSet())
{
HandleSelectDiscForDiscSet(entry->path);
HandleSelectDiscForDiscSet(entry->dbentry->disc_set);
return;
}
@ -8520,7 +8529,7 @@ void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
};
OpenChoiceDialog(
entry->GetDisplayTitle(), false, std::move(options),
entry->GetDisplayTitle(ShouldShowLocalizedTitles()), false, std::move(options),
[entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title,
bool checked) mutable {
switch (index)
@ -8561,31 +8570,32 @@ void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
}
else
{
// shouldn't fail
const GameList::Entry* first_disc_entry = GameList::GetFirstDiscSetMember(entry->path);
if (!first_disc_entry)
return;
ImGuiFullscreen::ChoiceDialogOptions options = {
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
{FSUI_ICONSTR(ICON_FA_IMAGE, "Set Cover Image"), false},
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc"), false},
};
OpenChoiceDialog(entry->title.c_str(), false, std::move(options),
[entry_path = first_disc_entry->path,
disc_set_name = entry->path](s32 index, const std::string& title, bool checked) mutable {
const GameDatabase::DiscSetEntry* dsentry = entry->dbentry->disc_set;
OpenChoiceDialog(entry->GetDisplayTitle(ShouldShowLocalizedTitles()), false, std::move(options),
[dsentry](s32 index, const std::string& title, bool checked) mutable {
switch (index)
{
case 0: // Open Game Properties
BeginTransition(
[entry_path = std::move(entry_path)]() { SwitchToGameSettingsForPath(entry_path); });
BeginTransition([dsentry]() {
// shouldn't fail
const GameList::Entry* first_disc_entry = GameList::GetFirstDiscSetMember(dsentry);
if (!first_disc_entry)
return;
SwitchToGameSettingsForPath(first_disc_entry->path);
});
break;
case 1: // Set Cover Image
DoSetCoverImage(std::move(disc_set_name));
DoSetCoverImage(std::string(dsentry->GetSaveTitle()));
break;
case 2: // Select Disc
HandleSelectDiscForDiscSet(disc_set_name);
HandleSelectDiscForDiscSet(dsentry);
break;
default:
break;
@ -8594,10 +8604,10 @@ void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
}
}
void FullscreenUI::HandleSelectDiscForDiscSet(std::string_view disc_set_name)
void FullscreenUI::HandleSelectDiscForDiscSet(const GameDatabase::DiscSetEntry* dsentry)
{
auto lock = GameList::GetLock();
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name, true);
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(dsentry, true);
if (entries.empty())
return;
@ -8615,7 +8625,8 @@ void FullscreenUI::HandleSelectDiscForDiscSet(std::string_view disc_set_name)
}
options.emplace_back(FSUI_ICONVSTR(ICON_FA_SQUARE_XMARK, "Close Menu"), false);
OpenChoiceDialog(fmt::format(FSUI_FSTR("Select Disc for {}"), disc_set_name), false, std::move(options),
OpenChoiceDialog(fmt::format(FSUI_FSTR("Select Disc for {}"), dsentry->GetDisplayTitle(ShouldShowLocalizedTitles())),
false, std::move(options),
[paths = std::move(paths)](s32 index, const std::string& title, bool checked) {
if (static_cast<u32>(index) >= paths.size())
return;
@ -8662,6 +8673,9 @@ void FullscreenUI::DrawGameListSettingsPage()
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_RECTANGLE_LIST, "Merge Multi-Disc Games"),
FSUI_VSTR("Merges multi-disc games into one item in the game list."), "Main",
"FullscreenUIMergeDiscSets", true);
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_LANGUAGE, "Show Localized Titles"),
FSUI_VSTR("Uses localized (native language) titles in the game list."), "Main",
"FullscreenUIShowLocalizedTitles", true);
if (DrawToggleSetting(
bsi, FSUI_ICONVSTR(ICON_FA_TROPHY, "Show Achievement Trophy Icons"),
FSUI_VSTR("Shows trophy icons in game grid when games have achievements or have been mastered."), "Main",
@ -10016,6 +10030,7 @@ TRANSLATE_NOOP("FullscreenUI", "Show GPU Statistics");
TRANSLATE_NOOP("FullscreenUI", "Show GPU Usage");
TRANSLATE_NOOP("FullscreenUI", "Show Grid View Titles");
TRANSLATE_NOOP("FullscreenUI", "Show Latency Statistics");
TRANSLATE_NOOP("FullscreenUI", "Show Localized Titles");
TRANSLATE_NOOP("FullscreenUI", "Show OSD Messages");
TRANSLATE_NOOP("FullscreenUI", "Show Resolution");
TRANSLATE_NOOP("FullscreenUI", "Show Speed");
@ -10129,6 +10144,7 @@ TRANSLATE_NOOP("FullscreenUI", "Uses PGXP for all instructions, not just memory
TRANSLATE_NOOP("FullscreenUI", "Uses a blit presentation model instead of flipping. This may be needed on some systems.");
TRANSLATE_NOOP("FullscreenUI", "Uses a second thread for drawing graphics. Provides a significant speed improvement particularly with the software renderer, and is safe to use.");
TRANSLATE_NOOP("FullscreenUI", "Uses game-specific settings for controllers for this game.");
TRANSLATE_NOOP("FullscreenUI", "Uses localized (native language) titles in the game list.");
TRANSLATE_NOOP("FullscreenUI", "Uses native resolution coordinates for 2D polygons, instead of precise coordinates. Can fix misaligned UI in some games, but otherwise should be left disabled.");
TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for colors, which can improve visuals in some games.");
TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for texture coordinates, straightening out warped textures.");

@ -40,7 +40,7 @@ namespace GameDatabase {
enum : u32
{
GAME_DATABASE_CACHE_SIGNATURE = 0x45434C48,
GAME_DATABASE_CACHE_VERSION = 29,
GAME_DATABASE_CACHE_VERSION = 30,
};
static const Entry* GetEntryForId(std::string_view code);
@ -50,8 +50,10 @@ static bool SaveToCache();
static bool LoadGameDBYaml();
static bool ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value);
static bool ParseYamlDiscSetEntry(DiscSetEntry* entry, const ryml::ConstNodeRef& value);
static bool ParseYamlCodes(PreferUnorderedStringMap<std::string_view>& lookup, const ryml::ConstNodeRef& value,
std::string_view serial);
static void BindDiscSetsToEntries();
static bool LoadTrackHashes();
static constexpr const std::array<const char*, static_cast<int>(CompatibilityRating::Count)>
@ -154,32 +156,46 @@ static constexpr std::array s_language_names = {
static_assert(s_language_names.size() == static_cast<size_t>(Language::MaxCount));
static constexpr const char* GAMEDB_YAML_FILENAME = "gamedb.yaml";
static constexpr const char* DISCSETS_YAML_FILENAME = "discsets.yaml";
static constexpr const char* DISCDB_YAML_FILENAME = "discdb.yaml";
static DisplayDeinterlacingMode DEFAULT_DEINTERLACING_MODE = DisplayDeinterlacingMode::Adaptive;
static bool s_loaded = false;
static bool s_track_hashes_loaded = false;
namespace {
struct ALIGN_TO_CACHE_LINE State
{
bool loaded;
bool track_hashes_loaded;
DynamicHeapArray<u8> db_data; // we take strings from the data, so store a copy
DynamicHeapArray<u8> disc_set_db_data; // if loaded from binary cache, this will be empty
std::vector<GameDatabase::Entry> entries;
std::vector<GameDatabase::DiscSetEntry> disc_sets;
PreferUnorderedStringMap<u32> code_lookup;
TrackHashesMap track_hashes_map;
};
} // namespace
static DynamicHeapArray<u8> s_db_data; // we take strings from the data, so store a copy
static std::vector<GameDatabase::Entry> s_entries;
static PreferUnorderedStringMap<u32> s_code_lookup;
static State s_state;
static TrackHashesMap s_track_hashes_map;
} // namespace GameDatabase
void GameDatabase::EnsureLoaded()
{
if (s_loaded)
if (s_state.loaded)
return;
Timer timer;
s_loaded = true;
s_state.loaded = true;
if (!LoadFromCache())
{
s_entries = {};
s_code_lookup = {};
s_state.entries = {};
s_state.code_lookup = {};
s_state.db_data.deallocate();
s_state.disc_set_db_data.deallocate();
if (LoadGameDBYaml())
{
@ -187,19 +203,21 @@ void GameDatabase::EnsureLoaded()
}
else
{
s_entries = {};
s_code_lookup = {};
s_state.entries = {};
s_state.code_lookup = {};
s_state.db_data.deallocate();
s_state.disc_set_db_data.deallocate();
}
}
INFO_LOG("Database load of {} entries took {:.0f}ms.", s_entries.size(), timer.GetTimeMilliseconds());
INFO_LOG("Database load of {} entries took {:.0f}ms.", s_state.entries.size(), timer.GetTimeMilliseconds());
}
void GameDatabase::Unload()
{
s_entries = {};
s_code_lookup = {};
s_loaded = false;
s_state.entries = {};
s_state.code_lookup = {};
s_state.loaded = false;
}
const GameDatabase::Entry* GameDatabase::GetEntryForId(std::string_view code)
@ -209,8 +227,8 @@ const GameDatabase::Entry* GameDatabase::GetEntryForId(std::string_view code)
EnsureLoaded();
auto iter = s_code_lookup.find(code);
return (iter != s_code_lookup.end()) ? &s_entries[iter->second] : nullptr;
auto iter = s_state.code_lookup.find(code);
return (iter != s_state.code_lookup.end()) ? &s_state.entries[iter->second] : nullptr;
}
std::string GameDatabase::GetSerialForDisc(CDImage* image)
@ -278,9 +296,9 @@ const GameDatabase::Entry* GameDatabase::GetEntryForSerial(std::string_view seri
EnsureLoaded();
const auto it =
std::lower_bound(s_entries.cbegin(), s_entries.cend(), serial,
std::lower_bound(s_state.entries.cbegin(), s_state.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;
return (it != s_state.entries.end() && it->serial == serial) ? &(*it) : nullptr;
}
const char* GameDatabase::GetTraitName(Trait trait)
@ -360,9 +378,9 @@ SmallString GameDatabase::Entry::GetLanguagesString() const
return ret;
}
std::string_view GameDatabase::Entry::GetDisplayTitle() const
std::string_view GameDatabase::Entry::GetDisplayTitle(bool localized) const
{
return !localized_title.empty() ? localized_title : title;
return (localized && !localized_title.empty()) ? localized_title : title;
}
std::string_view GameDatabase::Entry::GetSortTitle() const
@ -375,6 +393,31 @@ std::string_view GameDatabase::Entry::GetSaveTitle() const
return !save_title.empty() ? save_title : title;
}
bool GameDatabase::Entry::IsFirstDiscInSet() const
{
return (disc_set && disc_set->GetFirstSerial() == serial);
}
std::string_view GameDatabase::DiscSetEntry::GetDisplayTitle(bool localized) const
{
return (localized && !localized_title.empty()) ? localized_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetSortTitle() const
{
return !sort_title.empty() ? sort_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetSaveTitle() const
{
return !save_title.empty() ? save_title : title;
}
std::string_view GameDatabase::DiscSetEntry::GetFirstSerial() const
{
return serials[0];
}
void GameDatabase::Entry::ApplySettings(Settings& settings, bool display_osd_messages) const
{
if (display_active_start_offset.has_value())
@ -937,7 +980,7 @@ std::string GameDatabase::Entry::GenerateCompatibilityReport() const
if (!sort_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Sort Title"), title);
if (!localized_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Localized Title"), title);
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Localized Title"), localized_title);
if (!save_title.empty())
ret.append_format("**{}:** {}\n\n", TRANSLATE_SV("GameDatabase", "Save Title"), title);
@ -1015,10 +1058,10 @@ std::string GameDatabase::Entry::GenerateCompatibilityReport() const
if (settings_heading)
ret.append("\n");
if (!disc_set_name.empty())
if (disc_set)
{
ret.append_format("**{}:** {}\n", TRANSLATE_SV("GameDatabase", "Disc Set"), disc_set_name);
for (const std::string_view& ds_serial : disc_set_serials)
ret.append_format("**{}:** {}\n", TRANSLATE_SV("GameDatabase", "Disc Set"), disc_set->GetDisplayTitle(true));
for (const std::string_view& ds_serial : disc_set->serials)
ret.append_format(" - {}\n", ds_serial);
}
@ -1043,11 +1086,11 @@ bool GameDatabase::LoadFromCache()
BinarySpanReader reader(db_data->cspan());
const u64 gamedb_ts = Host::GetResourceFileTimestamp("gamedb.yaml", false).value_or(0);
u32 signature, version, num_entries, num_codes;
u32 signature, version, num_disc_set_entries, num_entries, num_codes;
u64 file_gamedb_ts;
if (!reader.ReadU32(&signature) || !reader.ReadU32(&version) || !reader.ReadU64(&file_gamedb_ts) ||
!reader.ReadU32(&num_entries) || !reader.ReadU32(&num_codes) || signature != GAME_DATABASE_CACHE_SIGNATURE ||
version != GAME_DATABASE_CACHE_VERSION)
!reader.ReadU32(&num_disc_set_entries) || !reader.ReadU32(&num_entries) || !reader.ReadU32(&num_codes) ||
signature != GAME_DATABASE_CACHE_SIGNATURE || version != GAME_DATABASE_CACHE_VERSION)
{
DEV_LOG("Cache header is corrupted or version mismatch.");
return false;
@ -1059,28 +1102,56 @@ bool GameDatabase::LoadFromCache()
return false;
}
s_entries.reserve(num_entries);
s_state.entries.reserve(num_entries);
s_state.disc_sets.reserve(num_disc_set_entries);
for (u32 i = 0; i < num_disc_set_entries; i++)
{
DiscSetEntry& disc_set = s_state.disc_sets.emplace_back();
u32 num_serials;
if (!reader.ReadSizePrefixedString(&disc_set.title) || !reader.ReadSizePrefixedString(&disc_set.sort_title) ||
!reader.ReadSizePrefixedString(&disc_set.localized_title) ||
!reader.ReadSizePrefixedString(&disc_set.save_title) || !reader.ReadU32(&num_serials))
{
DEV_LOG("Cache disc set entry is corrupted.");
return false;
}
if (num_serials > 0)
{
disc_set.serials.reserve(num_serials);
for (u32 j = 0; j < num_serials; j++)
{
if (!reader.ReadSizePrefixedString(&disc_set.serials.emplace_back()))
{
DEV_LOG("Cache disc set entry is corrupted.");
return false;
}
}
}
}
for (u32 i = 0; i < num_entries; i++)
{
Entry& entry = s_entries.emplace_back();
Entry& entry = s_state.entries.emplace_back();
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;
s32 disc_set_index;
if (!reader.ReadSizePrefixedString(&entry.serial) || !reader.ReadSizePrefixedString(&entry.title) ||
!reader.ReadSizePrefixedString(&entry.sort_title) || !reader.ReadSizePrefixedString(&entry.localized_title) ||
!reader.ReadSizePrefixedString(&entry.save_title) || !reader.ReadSizePrefixedString(&entry.genre) ||
!reader.ReadSizePrefixedString(&entry.developer) || !reader.ReadSizePrefixedString(&entry.publisher) ||
!reader.ReadSizePrefixedString(&entry.compatibility_version_tested) ||
!reader.ReadSizePrefixedString(&entry.compatibility_comments) || !reader.ReadU64(&entry.release_date) ||
!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.ReadSizePrefixedString(&entry.compatibility_comments) || !reader.ReadS32(&disc_set_index) ||
(disc_set_index >= 0 && static_cast<u32>(disc_set_index) >= num_disc_set_entries) ||
!reader.ReadU64(&entry.release_date) || !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(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) ||
@ -1092,26 +1163,13 @@ bool GameDatabase::LoadFromCache()
!reader.ReadOptionalT(&entry.gpu_max_run_ahead) || !reader.ReadOptionalT(&entry.gpu_pgxp_tolerance) ||
!reader.ReadOptionalT(&entry.gpu_pgxp_depth_threshold) ||
!reader.ReadOptionalT(&entry.gpu_pgxp_preserve_proj_fp) || !reader.ReadOptionalT(&entry.gpu_line_detect_mode) ||
!reader.ReadOptionalT(&entry.cpu_overclock) || !reader.ReadSizePrefixedString(&entry.disc_set_name) ||
!reader.ReadU32(&num_disc_set_serials))
!reader.ReadOptionalT(&entry.cpu_overclock))
{
DEV_LOG("Cache entry is corrupted.");
return false;
}
if (num_disc_set_serials > 0)
{
entry.disc_set_serials.reserve(num_disc_set_serials);
for (u32 j = 0; j < num_disc_set_serials; j++)
{
if (!reader.ReadSizePrefixedString(&entry.disc_set_serials.emplace_back()))
{
DEV_LOG("Cache entry is corrupted.");
return false;
}
}
}
entry.disc_set = (disc_set_index >= 0) ? &s_state.disc_sets[static_cast<u32>(disc_set_index)] : nullptr;
entry.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility);
entry.traits.reset();
for (size_t j = 0; j < static_cast<size_t>(Trait::MaxCount); j++)
@ -1130,16 +1188,17 @@ bool GameDatabase::LoadFromCache()
{
std::string code;
u32 index;
if (!reader.ReadSizePrefixedString(&code) || !reader.ReadU32(&index) || index >= static_cast<u32>(s_entries.size()))
if (!reader.ReadSizePrefixedString(&code) || !reader.ReadU32(&index) ||
index >= static_cast<u32>(s_state.entries.size()))
{
DEV_LOG("Cache code entry is corrupted.");
return false;
}
s_code_lookup.emplace(std::move(code), index);
s_state.code_lookup.emplace(std::move(code), index);
}
s_db_data = std::move(db_data.value());
s_state.db_data = std::move(db_data.value());
return true;
}
@ -1160,10 +1219,22 @@ bool GameDatabase::SaveToCache()
writer.WriteU32(GAME_DATABASE_CACHE_VERSION);
writer.WriteU64(static_cast<u64>(gamedb_ts));
writer.WriteU32(static_cast<u32>(s_entries.size()));
writer.WriteU32(static_cast<u32>(s_code_lookup.size()));
writer.WriteU32(static_cast<u32>(s_state.disc_sets.size()));
writer.WriteU32(static_cast<u32>(s_state.entries.size()));
writer.WriteU32(static_cast<u32>(s_state.code_lookup.size()));
for (const Entry& entry : s_entries)
for (const DiscSetEntry& disc_set : s_state.disc_sets)
{
writer.WriteSizePrefixedString(disc_set.title);
writer.WriteSizePrefixedString(disc_set.sort_title);
writer.WriteSizePrefixedString(disc_set.localized_title);
writer.WriteSizePrefixedString(disc_set.save_title);
writer.WriteU32(static_cast<u32>(disc_set.serials.size()));
for (const std::string_view& ds_serial : disc_set.serials)
writer.WriteSizePrefixedString(ds_serial);
}
for (const Entry& entry : s_state.entries)
{
writer.WriteSizePrefixedString(entry.serial);
writer.WriteSizePrefixedString(entry.title);
@ -1175,6 +1246,7 @@ bool GameDatabase::SaveToCache()
writer.WriteSizePrefixedString(entry.publisher);
writer.WriteSizePrefixedString(entry.compatibility_version_tested);
writer.WriteSizePrefixedString(entry.compatibility_comments);
writer.WriteS32(entry.disc_set ? static_cast<s32>(entry.disc_set - &s_state.disc_sets[0]) : -1);
writer.WriteU64(entry.release_date);
writer.WriteU8(entry.min_players);
writer.WriteU8(entry.max_players);
@ -1219,14 +1291,9 @@ bool GameDatabase::SaveToCache()
writer.WriteOptionalT(entry.gpu_pgxp_preserve_proj_fp);
writer.WriteOptionalT(entry.gpu_line_detect_mode);
writer.WriteOptionalT(entry.cpu_overclock);
writer.WriteSizePrefixedString(entry.disc_set_name);
writer.WriteU32(static_cast<u32>(entry.disc_set_serials.size()));
for (const std::string_view& serial : entry.disc_set_serials)
writer.WriteSizePrefixedString(serial);
}
for (const auto& it : s_code_lookup)
for (const auto& it : s_state.code_lookup)
{
writer.WriteSizePrefixedString(it.first);
writer.WriteU32(it.second);
@ -1245,62 +1312,90 @@ bool GameDatabase::LoadGameDBYaml()
return false;
}
const ryml::Tree tree = ryml::parse_in_place(
to_csubstr(GAMEDB_YAML_FILENAME), c4::substr(reinterpret_cast<char*>(gamedb_data->data()), gamedb_data->size()));
const ryml::ConstNodeRef root = tree.rootref();
s_entries.reserve(root.num_children());
std::optional<DynamicHeapArray<u8>> disc_set_data = Host::ReadResourceFile(DISCSETS_YAML_FILENAME, false, &error);
if (!disc_set_data.has_value())
{
ERROR_LOG("Failed to read disc set database: {}", error.GetDescription());
return false;
}
PreferUnorderedStringMap<std::string_view> code_lookup;
for (const ryml::ConstNodeRef& current : root.cchildren())
{
const std::string_view serial = to_stringview(current.key());
if (current.empty())
{
ERROR_LOG("Missing serial for entry.");
return false;
}
const ryml::Tree tree = ryml::parse_in_place(
to_csubstr(GAMEDB_YAML_FILENAME), c4::substr(reinterpret_cast<char*>(gamedb_data->data()), gamedb_data->size()));
const ryml::ConstNodeRef root = tree.rootref();
s_state.entries.reserve(root.num_children());
Entry& entry = s_entries.emplace_back();
entry.serial = serial;
if (!ParseYamlEntry(&entry, current))
for (const ryml::ConstNodeRef& current : root.cchildren())
{
s_entries.pop_back();
continue;
}
const std::string_view serial = to_stringview(current.key());
if (current.empty())
{
ERROR_LOG("Missing serial for entry.");
return false;
}
ParseYamlCodes(code_lookup, current, serial);
Entry& entry = s_state.entries.emplace_back();
entry.serial = serial;
if (!ParseYamlEntry(&entry, current))
{
s_state.entries.pop_back();
continue;
}
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(),
s_state.entries.shrink_to_fit();
std::sort(s_state.entries.begin(), s_state.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,
std::lower_bound(s_state.entries.cbegin(), s_state.entries.cend(), serial,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
if (it == s_entries.end() || it->serial != serial)
if (it == s_state.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)
if (!s_state.code_lookup.emplace(code, static_cast<u32>(std::distance(s_state.entries.cbegin(), it))).second)
ERROR_LOG("Failed to insert code {}", code);
}
if (s_entries.empty())
if (s_state.entries.empty())
{
ERROR_LOG("Game database is empty.");
return false;
}
s_db_data = std::move(gamedb_data.value());
// parse disc sets
{
const ryml::Tree tree =
ryml::parse_in_place(to_csubstr(DISCSETS_YAML_FILENAME),
c4::substr(reinterpret_cast<char*>(disc_set_data->data()), disc_set_data->size()));
const ryml::ConstNodeRef root = tree.rootref();
s_state.disc_sets.reserve(root.num_children());
for (const ryml::ConstNodeRef& current : root.cchildren())
{
DiscSetEntry& disc_set = s_state.disc_sets.emplace_back();
if (!ParseYamlDiscSetEntry(&disc_set, current))
{
s_state.disc_sets.pop_back();
continue;
}
}
s_state.disc_sets.shrink_to_fit();
BindDiscSetsToEntries();
}
s_state.db_data = std::move(gamedb_data.value());
s_state.disc_set_db_data = std::move(disc_set_data.value());
return true;
}
@ -1488,38 +1583,96 @@ bool GameDatabase::ParseYamlEntry(Entry* entry, const ryml::ConstNodeRef& value)
entry->cpu_overclock = GetOptionalTFromObject<u8>(settings, "cpuOverclockPercent");
}
if (const ryml::ConstNodeRef disc_set = value.find_child("discSet"); disc_set.valid() && disc_set.has_children())
return true;
}
bool GameDatabase::ParseYamlDiscSetEntry(DiscSetEntry* entry, const ryml::ConstNodeRef& value)
{
if (!GetStringFromObject(value, "name", &entry->title) || entry->title.empty())
{
GetStringFromObject(disc_set, "name", &entry->disc_set_name);
WARNING_LOG("Disc set entry is missing a title.");
return false;
}
GetStringFromObject(value, "sortName", &entry->sort_title);
GetStringFromObject(value, "localizedName", &entry->localized_title);
GetStringFromObject(value, "saveName", &entry->save_title);
// ensure there are no duplicates, linear search is okay here because there aren't that many disc sets
const std::string_view save_title = entry->GetSaveTitle();
const size_t search_count = s_state.disc_sets.size() - 1;
for (size_t i = 0; i < search_count; i++)
{
if (s_state.disc_sets[i].GetSaveTitle() == save_title)
{
WARNING_LOG("Duplicate disc set title {}.", save_title);
return false;
}
}
if (const ryml::ConstNodeRef set_serials = disc_set.find_child("serials");
set_serials.valid() && set_serials.has_children())
if (const ryml::ConstNodeRef set_serials = value.find_child("serials");
set_serials.valid() && set_serials.has_children())
{
entry->serials.reserve(set_serials.num_children());
for (const ryml::ConstNodeRef& serial : set_serials)
{
entry->disc_set_serials.reserve(set_serials.num_children());
for (const ryml::ConstNodeRef& serial : set_serials)
const std::string_view serial_str = to_stringview(serial.val());
if (serial_str.empty())
{
const std::string_view serial_str = to_stringview(serial.val());
if (serial_str.empty())
{
WARNING_LOG("Empty disc set serial in {}", entry->serial);
continue;
}
WARNING_LOG("Empty disc set serial in {}", entry->title);
continue;
}
if (std::find(entry->disc_set_serials.begin(), entry->disc_set_serials.end(), serial_str) !=
entry->disc_set_serials.end())
{
WARNING_LOG("Duplicate serial {} in disc set serials for {}", serial_str, entry->serial);
continue;
}
if (std::find(entry->serials.begin(), entry->serials.end(), serial_str) != entry->serials.end())
{
WARNING_LOG("Duplicate serial {} in disc set serials for {}", serial_str, entry->title);
continue;
}
entry->disc_set_serials.emplace_back(serial_str);
const auto entry_it =
std::lower_bound(s_state.entries.begin(), s_state.entries.end(), serial_str,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
if (entry_it == s_state.entries.end() || entry_it->serial != serial_str)
{
WARNING_LOG("Serial {} in disc set {} does not exist in game database.", serial_str, entry->title);
continue;
}
else if (entry_it->disc_set)
{
WARNING_LOG("Serial {} in disc set {} is already part of another disc set.", serial_str, entry->title);
continue;
}
entry->serials.emplace_back(serial_str);
}
}
if (entry->serials.empty())
{
WARNING_LOG("Disc set {} has {} serials, dropping.", entry->title, entry->serials.size());
return false;
}
return true;
}
void GameDatabase::BindDiscSetsToEntries()
{
for (DiscSetEntry& dsentry : s_state.disc_sets)
{
for (const std::string_view& serial : dsentry.serials)
{
const auto entry_it =
std::lower_bound(s_state.entries.begin(), s_state.entries.end(), serial,
[](const Entry& entry, const std::string_view& search) { return (entry.serial < search); });
Assert(entry_it != s_state.entries.end() && entry_it->serial == serial);
// link the entry to this disc set
entry_it->disc_set = &dsentry;
}
}
}
bool GameDatabase::ParseYamlCodes(PreferUnorderedStringMap<std::string_view>& lookup, const ryml::ConstNodeRef& value,
std::string_view serial)
{
@ -1564,10 +1717,10 @@ bool GameDatabase::ParseYamlCodes(PreferUnorderedStringMap<std::string_view>& lo
void GameDatabase::EnsureTrackHashesMapLoaded()
{
if (s_track_hashes_loaded)
if (s_state.track_hashes_loaded)
return;
s_track_hashes_loaded = true;
s_state.track_hashes_loaded = true;
LoadTrackHashes();
}
@ -1587,7 +1740,7 @@ bool GameDatabase::LoadTrackHashes()
const ryml::Tree tree = ryml::parse_in_arena(to_csubstr(DISCDB_YAML_FILENAME), to_csubstr(gamedb_data.value()));
const ryml::ConstNodeRef root = tree.rootref();
s_track_hashes_map = {};
s_state.track_hashes_map = {};
size_t serials = 0;
for (const ryml::ConstNodeRef& current : root.cchildren())
@ -1632,8 +1785,8 @@ bool GameDatabase::LoadTrackHashes()
const std::optional<CDImageHasher::Hash> md5o = CDImageHasher::HashFromString(md5_str);
if (md5o.has_value())
{
s_track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5o.value()),
std::forward_as_tuple(std::string(serial), revision_string, revision));
s_state.track_hashes_map.emplace(std::piecewise_construct, std::forward_as_tuple(md5o.value()),
std::forward_as_tuple(std::string(serial), revision_string, revision));
}
else
{
@ -1647,13 +1800,13 @@ bool GameDatabase::LoadTrackHashes()
}
ryml::reset_callbacks();
INFO_LOG("Loaded {} track hashes from {} serials in {:.0f}ms.", s_track_hashes_map.size(), serials,
INFO_LOG("Loaded {} track hashes from {} serials in {:.0f}ms.", s_state.track_hashes_map.size(), serials,
load_timer.GetTimeMilliseconds());
return !s_track_hashes_map.empty();
return !s_state.track_hashes_map.empty();
}
const GameDatabase::TrackHashesMap& GameDatabase::GetTrackHashesMap()
{
EnsureTrackHashesMapLoaded();
return s_track_hashes_map;
return s_state.track_hashes_map;
}

@ -96,6 +96,8 @@ enum class Language : u8
MaxCount,
};
struct DiscSetEntry;
struct Entry
{
static constexpr u16 SUPPORTS_MULTITAP_BIT = (1u << static_cast<u8>(ControllerType::Count));
@ -110,6 +112,7 @@ struct Entry
std::string_view publisher; ///< Publisher of the game.
std::string_view compatibility_version_tested; ///< Version of the application the game was tested with.
std::string_view compatibility_comments; ///< Comments about the game's compatibility.
const DiscSetEntry* disc_set; ///< Pointer to the disc set entry, if applicable.
u64 release_date; ///< Number of seconds since Epoch.
u8 min_players; ///< Minimum number of players supported.
u8 max_players; ///< Maximum number of players supported.
@ -138,9 +141,6 @@ struct Entry
std::optional<float> gpu_pgxp_depth_threshold; ///< GPU PGXP depth threshold override.
std::optional<bool> gpu_pgxp_preserve_proj_fp; ///< GPU PGXP preserve projection precision override.
std::string_view disc_set_name; ///< Name of the disc set, if applicable.
std::vector<std::string_view> disc_set_serials; ///< Serials of all discs in the set.
/// Checks if a trait is present.
ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast<int>(trait)]; }
@ -157,7 +157,7 @@ struct Entry
SmallString GetLanguagesString() const;
/// Returns the title that should be displayed for this game.
std::string_view GetDisplayTitle() const;
std::string_view GetDisplayTitle(bool localized) const;
/// Returns the sort name if present, otherwise the title.
std::string_view GetSortTitle() const;
@ -165,6 +165,9 @@ struct Entry
/// Returns the name to use when creating memory cards for this game.
std::string_view GetSaveTitle() const;
/// Returns true if we are the first disc in a disc set.
bool IsFirstDiscInSet() const;
/// Applies any settings overrides to the given settings object.
void ApplySettings(Settings& settings, bool display_osd_messages) const;
@ -172,6 +175,27 @@ struct Entry
std::string GenerateCompatibilityReport() const;
};
struct DiscSetEntry
{
std::string_view title; ///< Name of the disc set.
std::string_view sort_title; ///< Sort name of the disc set.
std::string_view localized_title; ///< Localized name of the disc set.
std::string_view save_title; ///< Name used for per-game memory cards.
std::vector<std::string_view> serials; ///< Serials of all discs in the set.
/// Returns the title that should be displayed for this game.
std::string_view GetDisplayTitle(bool localized) const;
/// Returns the sort name if present, otherwise the title.
std::string_view GetSortTitle() const;
/// Returns the name to use when creating memory cards for this game.
std::string_view GetSaveTitle() const;
/// Returns the first serial in the set.
std::string_view GetFirstSerial() const;
};
void EnsureLoaded();
void Unload();

@ -17,6 +17,7 @@
#include "util/image.h"
#include "util/ini_settings_interface.h"
#include "common/assert.h"
#include "common/binary_reader_writer.h"
#include "common/error.h"
#include "common/file_system.h"
@ -51,7 +52,7 @@ namespace {
enum : u32
{
GAME_LIST_CACHE_SIGNATURE = 0x45434C48,
GAME_LIST_CACHE_VERSION = 37,
GAME_LIST_CACHE_VERSION = 38,
PLAYED_TIME_SERIAL_LENGTH = 32,
PLAYED_TIME_LAST_TIME_LENGTH = 20, // uint64
@ -172,6 +173,11 @@ bool GameList::IsGameListLoaded()
return s_game_list_loaded;
}
bool GameList::ShouldShowLocalizedTitles()
{
return Host::GetBaseBoolSettingValue("UI", "GameListShowLocalizedTitles", true);
}
bool GameList::IsScannableFilename(std::string_view path)
{
// we don't scan bin files because they'll duplicate
@ -319,16 +325,14 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry)
{
// pull from database
entry->serial = dentry->serial;
entry->title = dentry->GetDisplayTitle();
entry->dbentry = dentry;
if (!cdi->HasSubImages())
if (!cdi->HasSubImages() && dentry->disc_set)
{
for (size_t i = 0; i < dentry->disc_set_serials.size(); i++)
for (size_t i = 0; i < dentry->disc_set->serials.size(); i++)
{
if (dentry->disc_set_serials[i] == entry->serial)
if (dentry->disc_set->serials[i] == entry->serial)
{
entry->disc_set_name = dentry->disc_set_name;
entry->disc_set_index = static_cast<s8>(i);
break;
}
@ -382,7 +386,6 @@ void GameList::MakeInvalidEntry(Entry* entry)
entry->path = {};
entry->serial = {};
entry->title = {};
entry->disc_set_name = {};
entry->dbentry = nullptr;
entry->hash = 0;
entry->file_size = 0;
@ -443,8 +446,7 @@ bool GameList::LoadEntriesFromCache(BinaryFileReader& reader)
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.ReadU64(&ge.hash) ||
!reader.ReadS64(&ge.file_size) || !reader.ReadU64(&ge.uncompressed_size) ||
!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) ||
!reader.Read(ge.achievements_hash.data(), ge.achievements_hash.size()) ||
region >= static_cast<u8>(DiscRegion::Count) || type > static_cast<u8>(EntryType::MaxCount))
@ -474,7 +476,6 @@ bool GameList::WriteEntryToCache(const Entry* entry, BinaryFileWriter& writer)
writer.WriteSizePrefixedString(entry->path);
writer.WriteSizePrefixedString(entry->serial);
writer.WriteSizePrefixedString(entry->title);
writer.WriteSizePrefixedString(entry->disc_set_name);
writer.WriteU64(entry->hash);
writer.WriteS64(entry->file_size);
writer.WriteU64(entry->uncompressed_size);
@ -518,7 +519,7 @@ bool GameList::LoadOrInitializeCache(std::FILE* fp, bool invalidate_cache)
return true;
}
static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const std::string& path)
static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const std::string_view& path)
{
return std::find_if(excluded_paths.begin(), excluded_paths.end(),
[&path](const std::string& entry) { return path.starts_with(entry); }) != excluded_paths.end();
@ -929,13 +930,15 @@ const GameList::Entry* GameList::GetEntryBySerialAndHash(std::string_view serial
return nullptr;
}
std::vector<const GameList::Entry*> GameList::GetDiscSetMembers(std::string_view disc_set_name,
std::vector<const GameList::Entry*> GameList::GetDiscSetMembers(const GameDatabase::DiscSetEntry* dsentry,
bool sort_by_most_recent)
{
Assert(dsentry);
std::vector<const Entry*> ret;
for (const Entry& entry : s_entries)
{
if (!entry.disc_set_member || disc_set_name != entry.disc_set_name)
if (!entry.disc_set_member || !entry.dbentry || entry.dbentry->disc_set != dsentry)
continue;
ret.push_back(&entry);
@ -959,11 +962,13 @@ std::vector<const GameList::Entry*> GameList::GetDiscSetMembers(std::string_view
return ret;
}
const GameList::Entry* GameList::GetFirstDiscSetMember(std::string_view disc_set_name)
const GameList::Entry* GameList::GetFirstDiscSetMember(const GameDatabase::DiscSetEntry* dsentry)
{
Assert(dsentry);
for (const Entry& entry : s_entries)
{
if (!entry.disc_set_member || disc_set_name != entry.disc_set_name)
if (!entry.disc_set_member || !entry.dbentry || entry.dbentry->disc_set != dsentry)
continue;
// Disc set should not have been created without the first disc being present.
@ -1084,24 +1089,16 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
const Entry& entry = s_entries[i];
// only first discs can create sets
if (entry.type != EntryType::Disc || entry.disc_set_member || entry.disc_set_index != 0)
continue;
// already have a disc set by this name?
const std::string& disc_set_name = entry.disc_set_name;
if (GetEntryForPath(disc_set_name))
continue;
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(entry.serial);
if (!dbentry)
if (entry.type != EntryType::Disc || !entry.dbentry || entry.disc_set_member || entry.disc_set_index != 0)
continue;
// need at least two discs for a set
const GameDatabase::DiscSetEntry* dsentry = entry.dbentry->disc_set;
bool found_another_disc = false;
for (const Entry& other_entry : s_entries)
{
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member ||
other_entry.disc_set_name != disc_set_name || other_entry.disc_set_index == entry.disc_set_index)
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member || !other_entry.dbentry ||
other_entry.dbentry->disc_set != dsentry || other_entry.disc_set_index == entry.disc_set_index)
{
continue;
}
@ -1110,7 +1107,7 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
}
if (!found_another_disc)
{
DEV_LOG("Not creating disc set {}, only one disc found", disc_set_name);
DEV_LOG("Not creating disc set {}, only one disc found", dsentry->title);
continue;
}
@ -1118,9 +1115,8 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
set_entry.dbentry = entry.dbentry;
set_entry.type = EntryType::DiscSet;
set_entry.region = entry.region;
set_entry.path = disc_set_name;
set_entry.path = entry.dbentry->disc_set->title;
set_entry.serial = entry.serial;
set_entry.title = entry.disc_set_name;
set_entry.hash = entry.hash;
set_entry.file_size = 0;
set_entry.uncompressed_size = 0;
@ -1134,7 +1130,7 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
// figure out play time for all discs, and sum it
// we do this via lookups, rather than the other entries, because of duplicates
for (const std::string_view& set_serial : dbentry->disc_set_serials)
for (const std::string_view& set_serial : dsentry->serials)
{
const auto it = played_time_map.find(set_serial);
if (it == played_time_map.end())
@ -1152,13 +1148,13 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
u32 num_parts = 0;
for (Entry& other_entry : s_entries)
{
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member ||
other_entry.disc_set_name != disc_set_name)
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member || !other_entry.dbentry ||
other_entry.dbentry->disc_set != dsentry)
{
continue;
}
DEV_LOG("Adding {} to disc set {}", Path::GetFileName(other_entry.path), disc_set_name);
DEV_LOG("Adding {} to disc set {}", Path::GetFileName(other_entry.path), dsentry->title);
other_entry.disc_set_member = true;
set_entry.last_modified_time = std::min(set_entry.last_modified_time, other_entry.last_modified_time);
set_entry.file_size += other_entry.file_size;
@ -1166,10 +1162,10 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
num_parts++;
}
DEV_LOG("Created disc set {} from {} entries", disc_set_name, num_parts);
DEV_LOG("Created disc set {} from {} entries", dsentry->title, num_parts);
// we have to do the exclusion check at the end, because otherwise the individual discs get added
if (!IsPathExcluded(excluded_paths, disc_set_name))
if (!IsPathExcluded(excluded_paths, dsentry->title))
s_entries.push_back(std::move(set_entry));
}
}
@ -1244,33 +1240,40 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha
}
// Check for illegal characters, use serial instead.
const std::string sanitized_name(Path::SanitizeFileName(entry->title));
const std::string_view save_title = entry->GetSaveTitle();
const std::string sanitized_name = Path::SanitizeFileName(save_title);
std::string name;
if (sanitized_name != entry->title || use_serial)
if (sanitized_name != save_title || use_serial)
name = fmt::format("{}{}", entry->serial, extension);
else
name = fmt::format("{}{}", entry->title, extension);
name = fmt::format("{}{}", save_title, extension);
return Path::Combine(EmuFolders::Covers, Path::SanitizeFileName(name));
}
std::string_view GameList::Entry::GetDisplayTitle() const
std::string_view GameList::Entry::GetDisplayTitle(bool localized) const
{
// if custom title is present, use that for display too
return has_custom_title ? title : (dbentry ? dbentry->GetDisplayTitle() : title);
return !title.empty() ? std::string_view(title) :
(IsDiscSet() ? dbentry->disc_set->GetDisplayTitle(localized) :
(dbentry ? dbentry->GetDisplayTitle(localized) : std::string_view()));
}
std::string_view GameList::Entry::GetSortTitle() const
{
// if custom title is present, use that for sorting too
return has_custom_title ? title : (dbentry ? dbentry->GetSortTitle() : title);
return !title.empty() ?
std::string_view(title) :
(IsDiscSet() ? dbentry->disc_set->GetSortTitle() : (dbentry ? dbentry->GetSortTitle() : std::string_view()));
}
std::string_view GameList::Entry::GetSaveTitle() const
{
// if custom title is present, use that for save folder too
return has_custom_title ? title : (dbentry ? dbentry->GetSaveTitle() : title);
return !title.empty() ?
std::string_view(title) :
(IsDiscSet() ? dbentry->disc_set->GetSaveTitle() : (dbentry ? dbentry->GetSaveTitle() : std::string_view()));
}
std::string_view GameList::Entry::GetLanguageIcon() const
@ -1475,7 +1478,7 @@ void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t las
}
else if (entry.IsDiscSet())
{
if (!dbentry || entry.path != dbentry->disc_set_name)
if (!dbentry || entry.dbentry->disc_set != dbentry->disc_set)
continue;
// have to add here, because other discs are already included in the sum
@ -1514,7 +1517,7 @@ void GameList::ClearPlayedTimeForEntry(const GameList::Entry* entry)
if (entry->IsDiscSet())
{
for (const GameList::Entry* member : GetDiscSetMembers(entry->path))
for (const GameList::Entry* member : GetDiscSetMembers(entry->dbentry->disc_set))
{
if (!member->serial.empty())
serials.push_back(member->serial);
@ -1631,12 +1634,12 @@ TinyString GameList::FormatTimespan(std::time_t timespan, bool long_format)
}
std::vector<std::pair<std::string_view, const GameList::Entry*>>
GameList::GetMatchingEntriesForSerial(const std::span<const std::string_view> serials)
GameList::GetEntriesInDiscSet(const GameDatabase::DiscSetEntry* dsentry, bool localized_titles)
{
std::vector<std::pair<std::string_view, const GameList::Entry*>> ret;
ret.reserve(serials.size());
ret.reserve(dsentry->serials.size());
for (const std::string_view& serial : serials)
for (const std::string_view& serial : dsentry->serials)
{
const Entry* matching_entry = nullptr;
bool has_multiple_entries = false;
@ -1657,7 +1660,7 @@ GameList::GetMatchingEntriesForSerial(const std::span<const std::string_view> se
if (!has_multiple_entries)
{
ret.emplace_back(matching_entry->title, matching_entry);
ret.emplace_back(matching_entry->GetDisplayTitle(localized_titles), matching_entry);
continue;
}
@ -1681,20 +1684,27 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
progress = ProgressCallback::NullProgressCallback;
bool has_title = false;
bool has_localized_title = false;
bool has_save_title = false;
bool has_file_title = false;
bool has_serial = false;
for (const std::string& url_template : url_templates)
{
if (!has_title && url_template.find("${title}") != std::string::npos)
has_title = true;
if (!has_title && url_template.find("${localizedtitle}") != std::string::npos)
has_localized_title = true;
if (!has_title && url_template.find("${savetitle}") != std::string::npos)
has_save_title = true;
if (!has_file_title && url_template.find("${filetitle}") != std::string::npos)
has_file_title = true;
if (!has_serial && url_template.find("${serial}") != std::string::npos)
has_serial = true;
}
if (!has_title && !has_file_title && !has_serial)
if (!has_title && !has_save_title && !has_file_title && !has_serial)
{
progress->DisplayError("URL template must contain at least one of ${title}, ${filetitle}, or ${serial}.");
progress->DisplayError(TRANSLATE_SV(
"GameList", "URL template must contain at least one of ${title}, ${savetitle}, ${filetitle}, or ${serial}."));
return false;
}
@ -1711,7 +1721,11 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
{
std::string url(url_template);
if (has_title)
StringUtil::ReplaceAll(&url, "${title}", Path::URLEncode(entry.title));
StringUtil::ReplaceAll(&url, "${title}", Path::URLEncode(entry.GetDisplayTitle(false)));
if (has_localized_title)
StringUtil::ReplaceAll(&url, "${localizedtitle}", Path::URLEncode(entry.GetDisplayTitle(true)));
if (has_save_title)
StringUtil::ReplaceAll(&url, "${savetitle}", Path::URLEncode(entry.GetSaveTitle()));
if (has_file_title)
{
std::string display_name(FileSystem::GetDisplayNameFromPath(entry.path));
@ -1726,7 +1740,7 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
}
if (download_urls.empty())
{
progress->DisplayError("No URLs to download enumerated.");
progress->DisplayError(TRANSLATE_SV("GameList", "No URLs to download enumerated."));
return false;
}
@ -1734,7 +1748,8 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
std::unique_ptr<HTTPDownloader> downloader(HTTPDownloader::Create(Host::GetHTTPUserAgent(), &error));
if (!downloader)
{
progress->DisplayError(fmt::format("Failed to create HTTP downloader:\n{}", error.GetDescription()));
progress->DisplayError(
fmt::format(TRANSLATE_FS("GameList", "Failed to create HTTP downloader:\n{}"), error.GetDescription()));
return false;
}
@ -1756,7 +1771,7 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
continue;
}
progress->SetStatusText(entry->title);
progress->SetStatusText(entry->GetDisplayTitle(true));
}
// we could actually do a few in parallel here...

@ -43,7 +43,6 @@ struct Entry
std::string path;
std::string serial;
std::string title;
std::string disc_set_name;
const GameDatabase::Entry* dbentry = nullptr;
@ -60,7 +59,7 @@ struct Entry
u16 unlocked_achievements = 0;
u16 unlocked_achievements_hc = 0;
std::string_view GetDisplayTitle() const;
std::string_view GetDisplayTitle(bool localized) const;
std::string_view GetSortTitle() const;
@ -78,6 +77,10 @@ struct Entry
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
ALWAYS_INLINE bool HasCustomLanguage() const { return (custom_language != GameDatabase::Language::MaxCount); }
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
ALWAYS_INLINE const GameDatabase::DiscSetEntry* GetDiscSetEntry() const
{
return dbentry ? dbentry->disc_set : nullptr;
}
ALWAYS_INLINE bool AreAchievementsMastered() const
{
return (num_achievements > 0 &&
@ -104,11 +107,13 @@ const Entry* GetEntryByIndex(u32 index);
const Entry* GetEntryForPath(std::string_view path);
const Entry* GetEntryBySerial(std::string_view serial);
const Entry* GetEntryBySerialAndHash(std::string_view serial, u64 hash);
std::vector<const Entry*> GetDiscSetMembers(std::string_view disc_set_name, bool sort_by_most_recent = false);
const Entry* GetFirstDiscSetMember(std::string_view disc_set_name);
std::vector<const Entry*> GetDiscSetMembers(const GameDatabase::DiscSetEntry* dsentry,
bool sort_by_most_recent = false);
const Entry* GetFirstDiscSetMember(const GameDatabase::DiscSetEntry* dsentry);
u32 GetEntryCount();
bool IsGameListLoaded();
bool ShouldShowLocalizedTitles();
/// Populates the game list with files in the configured directories.
/// If invalidate_cache is set, all files will be re-scanned.
@ -143,8 +148,8 @@ std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_fil
/// Returns a list of (title, entry) for entries matching serials. Titles will match the gamedb title,
/// except when two files have the same serial, in which case the filename will be used instead.
std::vector<std::pair<std::string_view, const Entry*>>
GetMatchingEntriesForSerial(const std::span<const std::string_view> serials);
std::vector<std::pair<std::string_view, const Entry*>> GetEntriesInDiscSet(const GameDatabase::DiscSetEntry* dsentry,
bool localized_titles);
/// Downloads covers using the specified URL templates. By default, covers are saved by title, but this can be changed
/// with the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to.

@ -3547,14 +3547,14 @@ std::string GPUTextureCache::GetTextureReplacementDirectory()
{
// If this is a multi-disc game, try the first disc.
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(serial);
if (dbentry && !dbentry->disc_set_serials.empty() && serial != dbentry->disc_set_serials.front())
if (dbentry && dbentry->disc_set && serial != dbentry->disc_set->serials.front())
{
altdir =
Path::Combine(EmuFolders::Textures, SmallString::from_format("{}" FS_OSPATH_SEPARATOR_STR "replacements",
dbentry->disc_set_serials.front()));
dbentry->disc_set->serials.front()));
if (FileSystem::DirectoryExists(altdir.c_str()))
{
WARNING_LOG("Using texture replacements from first disc {}", dbentry->disc_set_serials.front());
WARNING_LOG("Using texture replacements from first disc {}", dbentry->disc_set->serials.front());
dir = std::move(altdir);
}
}

@ -1417,8 +1417,7 @@ std::string System::GetGameSettingsPath(std::string_view game_serial, bool ignor
{
// multi-disc games => always use the first disc
const GameDatabase::Entry* entry = ignore_disc_set ? nullptr : GameDatabase::GetEntryForSerial(game_serial);
const std::string_view serial_for_path =
(entry && !entry->disc_set_serials.empty()) ? entry->disc_set_serials.front() : game_serial;
const std::string_view serial_for_path = (entry && entry->disc_set) ? entry->disc_set->GetFirstSerial() : game_serial;
return Path::Combine(EmuFolders::GameSettings, fmt::format("{}.ini", Path::SanitizeFileName(serial_for_path)));
}
@ -1438,14 +1437,14 @@ std::unique_ptr<INISettingsInterface> System::GetGameSettingsInterface(const Gam
if (ret->Load(&error))
{
// Check for separate disc configuration.
if (dbentry && !dbentry->disc_set_serials.empty() && dbentry->disc_set_serials.front() != serial)
if (dbentry && !dbentry->IsFirstDiscInSet())
{
if (ret->GetBoolValue("Main", "UseSeparateConfigForDiscSet", false))
{
if (!quiet)
{
INFO_COLOR_LOG(StrongCyan, "Using separate disc game settings serial {} for disc set {}", serial,
dbentry->disc_set_serials.front());
dbentry->disc_set->title);
}
// Load the disc specific ini.
@ -3817,10 +3816,10 @@ std::unique_ptr<MemoryCard> System::GetMemoryCardForSlot(u32 slot, MemoryCardTyp
card_path = g_settings.GetGameMemoryCardPath(Path::SanitizeFileName(s_state.running_game_title), slot);
}
// Multi-disc game - use disc set name.
else if (s_state.running_game_entry && !s_state.running_game_entry->disc_set_name.empty())
else if (s_state.running_game_entry && s_state.running_game_entry->disc_set)
{
card_path =
g_settings.GetGameMemoryCardPath(Path::SanitizeFileName(s_state.running_game_entry->disc_set_name), slot);
card_path = g_settings.GetGameMemoryCardPath(
Path::SanitizeFileName(s_state.running_game_entry->disc_set->GetSaveTitle()), slot);
}
// But prefer a disc-specific card if one already exists.
@ -4153,9 +4152,14 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo
{
s_state.running_game_entry = GameDatabase::GetEntryForSerial(s_state.running_game_serial);
if (s_state.running_game_entry && s_state.running_game_title.empty())
s_state.running_game_title = s_state.running_game_entry->GetDisplayTitle();
{
s_state.running_game_title =
s_state.running_game_entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles());
}
else if (s_state.running_game_title.empty())
{
s_state.running_game_title = s_state.running_game_serial;
}
}
}
}
@ -4172,7 +4176,10 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo
{
s_state.running_game_serial = s_state.running_game_entry->serial;
if (s_state.running_game_title.empty())
s_state.running_game_title = s_state.running_game_entry->GetDisplayTitle();
{
s_state.running_game_title =
s_state.running_game_entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles());
}
}
else
{
@ -4355,7 +4362,7 @@ bool System::SwitchMediaSubImage(u32 index)
bool System::SwitchDiscFromSet(s32 direction, bool display_osd_message)
{
if (!IsValid() || !s_state.running_game_entry || s_state.running_game_entry->disc_set_serials.empty())
if (!IsValid() || !s_state.running_game_entry || !s_state.running_game_entry->disc_set)
{
if (display_osd_message)
{
@ -4367,17 +4374,17 @@ bool System::SwitchDiscFromSet(s32 direction, bool display_osd_message)
return false;
}
s32 current_index = static_cast<s32>(s_state.running_game_entry->disc_set_serials.size());
for (size_t i = 0; i < s_state.running_game_entry->disc_set_serials.size(); i++)
s32 current_index = static_cast<s32>(s_state.running_game_entry->disc_set->serials.size());
for (size_t i = 0; i < s_state.running_game_entry->disc_set->serials.size(); i++)
{
if (s_state.running_game_entry->disc_set_serials[i] == s_state.running_game_serial)
if (s_state.running_game_entry->disc_set->serials[i] == s_state.running_game_serial)
{
current_index = static_cast<s32>(i);
break;
}
}
if (current_index == static_cast<s32>(s_state.running_game_entry->disc_set_serials.size()))
if (current_index == static_cast<s32>(s_state.running_game_entry->disc_set->serials.size()))
{
if (display_osd_message)
{
@ -4390,7 +4397,7 @@ bool System::SwitchDiscFromSet(s32 direction, bool display_osd_message)
}
current_index += direction;
if (current_index < 0 || current_index >= static_cast<s32>(s_state.running_game_entry->disc_set_serials.size()))
if (current_index < 0 || current_index >= static_cast<s32>(s_state.running_game_entry->disc_set->serials.size()))
{
if (display_osd_message)
{
@ -4403,7 +4410,7 @@ bool System::SwitchDiscFromSet(s32 direction, bool display_osd_message)
return false;
}
const std::string_view& next_serial = s_state.running_game_entry->disc_set_serials[current_index];
const std::string_view& next_serial = s_state.running_game_entry->disc_set->serials[current_index];
const auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryBySerial(next_serial);
if (!entry)
@ -5777,8 +5784,8 @@ std::string System::GetGameMemoryCardPath(std::string_view serial, std::string_v
const bool global_use_playlist_title = Host::GetBaseBoolSettingValue(section, "UsePlaylistTitle", true);
const bool use_playlist_title =
ini ? ini->GetBoolValue(section, "UsePlaylistTitle", global_use_playlist_title) : global_use_playlist_title;
if (!entry->disc_set_name.empty() && use_playlist_title && !FileSystem::FileExists(ret.c_str()))
ret = g_settings.GetGameMemoryCardPath(Path::SanitizeFileName(entry->disc_set_name), slot);
if (entry->disc_set && use_playlist_title && !FileSystem::FileExists(ret.c_str()))
ret = g_settings.GetGameMemoryCardPath(Path::SanitizeFileName(entry->disc_set->GetSaveTitle()), slot);
}
else
{
@ -6188,10 +6195,9 @@ void System::UpdateRichPresence(bool update_session_time)
if (IsValidOrInitializing())
{
// Use disc set name if it's not a custom title.
if (s_state.running_game_entry && !s_state.running_game_entry->disc_set_name.empty() &&
s_state.running_game_title == s_state.running_game_entry->GetDisplayTitle())
if (!s_state.running_game_custom_title && s_state.running_game_entry && s_state.running_game_entry->disc_set)
{
game_details = s_state.running_game_entry->disc_set_name;
game_details = s_state.running_game_entry->disc_set->GetDisplayTitle(true);
}
else
{

@ -47,7 +47,7 @@
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game's filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${savetitle}:&lt;/span&gt; Title of the game including the region.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${localizedtitle}:&lt;/span&gt; Localized (native language) title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game's filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>

@ -127,6 +127,7 @@ GameListModel::GameListModel(QObject* parent)
: QAbstractTableModel(parent), m_memcard_pixmap_cache(MIN_COVER_CACHE_SIZE)
{
m_cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
m_show_localized_titles = GameList::ShouldShowLocalizedTitles();
m_show_titles_for_covers = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
m_show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true);
@ -142,6 +143,22 @@ GameListModel::GameListModel(QObject* parent)
GameListModel::~GameListModel() = default;
void GameListModel::setShowLocalizedTitles(bool enabled)
{
if (m_show_localized_titles == enabled)
return;
m_show_localized_titles = enabled;
Host::SetBaseBoolSettingValue("UI", "GameListShowLocalizedTitles", enabled);
Host::CommitBaseSettingChanges();
// emit cover changed as well since the autogenerated covers will differ
emit dataChanged(index(0, Column_Title), index(rowCount() - 1, Column_Title), {Qt::DisplayRole});
m_cover_pixmap_cache.Clear();
refreshCovers();
}
void GameListModel::setShowGameIcons(bool enabled)
{
m_show_game_icons = enabled;
@ -209,7 +226,7 @@ void GameListModel::updateCoverScale()
void GameListModel::refreshCovers()
{
m_cover_pixmap_cache.Clear();
refresh();
emit dataChanged(index(0, Column_Cover), index(rowCount() - 1, Column_Cover), {Qt::DisplayRole});
}
void GameListModel::updateCacheSize(int num_rows, int num_columns)
@ -229,18 +246,19 @@ void GameListModel::reloadThemeSpecificImages()
void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
{
QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, title = ge->title,
QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, save_title = std::string(ge->GetSaveTitle()),
display_title = QtUtils::StringViewToQString(ge->GetDisplayTitle(m_show_localized_titles)),
placeholder_image = m_placeholder_image, list = this, width = getCoverArtSize(),
height = getCoverArtSize(), scale = m_cover_scale,
dpr = qApp->devicePixelRatio()]() mutable {
QImage image;
loadOrGenerateCover(image, placeholder_image, width, height, scale, dpr, path, serial, title);
loadOrGenerateCover(image, placeholder_image, width, height, scale, dpr, path, serial, save_title, display_title);
return [path = std::move(path), image = std::move(image), list, scale]() { list->coverLoaded(path, image, scale); };
});
}
void GameListModel::createPlaceholderImage(QImage& image, const QImage& placeholder_image, int width, int height,
float scale, const std::string& title)
float scale, const QString& title)
{
image = placeholder_image.copy();
if (image.isNull())
@ -258,16 +276,16 @@ void GameListModel::createPlaceholderImage(QImage& image, const QImage& placehol
const QRect text_rc(0, 0, static_cast<int>(static_cast<float>(width)),
static_cast<int>(static_cast<float>(height)));
painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title));
painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, title);
painter.end();
}
}
void GameListModel::loadOrGenerateCover(QImage& image, const QImage& placeholder_image, int width, int height,
float scale, float dpr, const std::string& path, const std::string& serial,
const std::string& title)
const std::string& save_title, const QString& display_title)
{
const std::string cover_path(GameList::GetCoverImagePath(path, serial, title));
const std::string cover_path(GameList::GetCoverImagePath(path, serial, save_title));
if (!cover_path.empty())
{
image.load(QString::fromStdString(cover_path));
@ -279,7 +297,7 @@ void GameListModel::loadOrGenerateCover(QImage& image, const QImage& placeholder
}
if (image.isNull())
createPlaceholderImage(image, placeholder_image, width, height, scale, title);
createPlaceholderImage(image, placeholder_image, width, height, scale, display_title);
}
void GameListModel::coverLoaded(const std::string& path, const QImage& image, float scale)
@ -529,7 +547,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList:
return QtUtils::StringViewToQString(ge->serial);
case Column_Title:
return QtUtils::StringViewToQString(ge->GetDisplayTitle());
return QtUtils::StringViewToQString(ge->GetDisplayTitle(m_show_localized_titles));
case Column_FileTitle:
return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
@ -589,7 +607,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList:
case Column_Cover:
{
if (m_show_titles_for_covers)
return QtUtils::StringViewToQString(ge->GetDisplayTitle());
return QtUtils::StringViewToQString(ge->GetDisplayTitle(m_show_localized_titles));
else
return {};
}
@ -665,7 +683,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList:
return QtUtils::StringViewToQString(ge->serial);
case Column_Title:
return QtUtils::StringViewToQString(ge->GetDisplayTitle());
return QtUtils::StringViewToQString(ge->GetDisplayTitle(m_show_localized_titles));
case Column_FileTitle:
return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
@ -1072,7 +1090,8 @@ public:
{
if (!((!entry->IsDiscSet() && StringUtil::ContainsNoCase(entry->path, m_filter_name)) ||
StringUtil::ContainsNoCase(entry->serial, m_filter_name) ||
StringUtil::ContainsNoCase(entry->GetDisplayTitle(), m_filter_name)))
StringUtil::ContainsNoCase(entry->GetDisplayTitle(true), m_filter_name) ||
StringUtil::ContainsNoCase(entry->GetDisplayTitle(false), m_filter_name)))
{
return false;
}
@ -1219,7 +1238,8 @@ GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent
GameListWidget::~GameListWidget() = default;
void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid, QAction* actionMergeDiscSets,
QAction* actionListShowIcons, QAction* actionGridShowTitles)
QAction* actionListShowIcons, QAction* actionGridShowTitles,
QAction* actionShowLocalizedTitles)
{
m_model = new GameListModel(this);
connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged);
@ -1256,6 +1276,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
m_ui.mergeDiscSets->setDefaultAction(actionMergeDiscSets);
m_ui.showGameIcons->setDefaultAction(actionListShowIcons);
m_ui.showGridTitles->setDefaultAction(actionGridShowTitles);
m_ui.showLocalizedTitles->setDefaultAction(actionShowLocalizedTitles);
connect(m_ui.gridScale, &QSlider::valueChanged, m_grid_view, &GameListGridView::setZoomPct);
connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
@ -1288,6 +1309,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
else
actionGameList->setChecked(true);
actionMergeDiscSets->setChecked(m_sort_model->isMergingDiscSets());
actionShowLocalizedTitles->setChecked(m_model->getShowLocalizedTitles());
actionListShowIcons->setChecked(m_model->getShowGameIcons());
actionGridShowTitles->setChecked(m_model->getShowCoverTitles());
onCoverScaleChanged(m_model->getCoverScale());
@ -1446,8 +1468,9 @@ void GameListWidget::onListViewItemActivated(const QModelIndex& index)
const GameList::Entry* entry = GameList::GetEntryByIndex(static_cast<u32>(source_index.row()));
if (entry)
{
SettingsWindow::openGamePropertiesDialog(entry->path, std::string(entry->GetDisplayTitle()), entry->serial,
entry->hash, entry->region);
SettingsWindow::openGamePropertiesDialog(entry->path,
std::string(entry->GetDisplayTitle(m_model->getShowLocalizedTitles())),
entry->serial, entry->hash, entry->region);
}
}
else
@ -1541,6 +1564,18 @@ void GameListWidget::setMergeDiscSets(bool enabled)
m_model->refreshIcons();
}
void GameListWidget::setShowLocalizedTitles(bool enabled)
{
if (m_model->getShowLocalizedTitles() == enabled)
return;
Host::SetBaseBoolSettingValue("UI", "GameListShowLocalizedTitles", enabled);
Host::CommitBaseSettingChanges();
m_model->setShowLocalizedTitles(enabled);
if (isShowingGameGrid())
m_model->refresh();
}
void GameListWidget::setShowGameIcons(bool enabled)
{
if (m_model->getShowGameIcons() == enabled)

@ -87,6 +87,9 @@ public:
bool lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column) const;
bool getShowLocalizedTitles() const { return m_show_localized_titles; }
void setShowLocalizedTitles(bool enabled);
bool getShowCoverTitles() const { return m_show_titles_for_covers; }
void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; }
@ -119,17 +122,17 @@ private:
static void loadOrGenerateCover(QImage& image, const QImage& placeholder_image, int width, int height, float scale,
float dpr, const std::string& path, const std::string& serial,
const std::string& title);
const std::string& save_title, const QString& display_title);
static void createPlaceholderImage(QImage& image, const QImage& placeholder_image, int width, int height, float scale,
const std::string& title);
const QString& title);
const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const;
const QPixmap& getFlagPixmapForEntry(const GameList::Entry* ge) const;
static void fixIconPixmapSize(QPixmap& pm);
std::optional<GameList::EntryList> m_taken_entries;
float m_cover_scale = 0.0f;
bool m_show_localized_titles = false;
bool m_show_titles_for_covers = false;
bool m_show_game_icons = false;
@ -220,7 +223,7 @@ public:
ALWAYS_INLINE GameListGridView* getGridView() const { return m_grid_view; }
void initialize(QAction* actionGameList, QAction* actionGameGrid, QAction* actionMergeDiscSets,
QAction* actionListShowIcons, QAction* actionGridShowTitles);
QAction* actionListShowIcons, QAction* actionGridShowTitles, QAction* actionLocalizedTitles);
void refresh(bool invalidate_cache);
void cancelRefresh();
@ -259,6 +262,7 @@ public Q_SLOTS:
void showGameList();
void showGameGrid();
void setMergeDiscSets(bool enabled);
void setShowLocalizedTitles(bool enabled);
void setShowGameIcons(bool enabled);
void setShowCoverTitles(bool enabled);
void refreshGridCovers();

@ -27,7 +27,7 @@
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,0,1,0,0,0">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,0,0,1,0,0,0">
<property name="spacing">
<number>3</number>
</property>
@ -100,6 +100,25 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="showLocalizedTitles">
<property name="minimumSize">
<size>
<width>32</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Show Localized Icons</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="showGameIcons">
<property name="minimumSize">
@ -168,6 +187,12 @@
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>

@ -126,7 +126,7 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
if (entry)
{
m_ui.title->setText(QtUtils::StringViewToQString(entry->GetDisplayTitle()));
m_ui.title->setText(QtUtils::StringViewToQString(entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles())));
m_ui.compatibility->setCurrentIndex(static_cast<int>(entry->compatibility));
m_ui.genre->setText(entry->genre.empty() ? tr("Unknown") : QtUtils::StringViewToQString(entry->genre));
if (!entry->developer.empty() && !entry->publisher.empty() && entry->developer != entry->publisher)
@ -207,9 +207,9 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
m_ui.entryType->setCurrentIndex(static_cast<int>(gentry->type));
}
if (entry && !entry->disc_set_serials.empty())
if (entry && entry->disc_set)
{
if (serial == entry->disc_set_serials.front())
if (entry->IsFirstDiscInSet())
{
m_ui.separateDiscSettings->setCheckState(
m_dialog->getBoolValue("Main", "UseSeparateConfigForDiscSet", std::nullopt).value_or(false) ? Qt::Checked :
@ -266,17 +266,19 @@ void GameSummaryWidget::populateCustomAttributes()
return;
}
if (entry->has_custom_title)
{
QSignalBlocker sb(m_ui.title);
m_ui.title->setText(QtUtils::StringViewToQString(entry->GetDisplayTitle()));
m_ui.restoreTitle->setEnabled(entry->has_custom_title);
m_ui.title->setText(QtUtils::StringViewToQString(entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles())));
}
m_ui.restoreTitle->setEnabled(entry->has_custom_title);
if (entry->has_custom_region)
{
QSignalBlocker sb(m_ui.region);
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
}
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
{
QSignalBlocker sb(m_ui.customLanguage);

@ -1104,7 +1104,7 @@ const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* en
return entry;
// disc set... need to figure out the disc we want
SelectDiscDialog dlg(entry->path, this);
SelectDiscDialog dlg(entry->GetDiscSetEntry(), m_game_list_widget->getModel()->getShowLocalizedTitles(), this);
lock.unlock();
const int res = dlg.exec();
@ -1299,10 +1299,11 @@ void MainWindow::onChangeDiscMenuAboutToShow()
connect(action, &QAction::triggered, [i]() { g_emu_thread->changeDiscFromPlaylist(i); });
}
}
else if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && !entry->disc_set_serials.empty())
else if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && entry->disc_set)
{
auto lock = GameList::GetLock();
for (const auto& [title, glentry] : GameList::GetMatchingEntriesForSerial(entry->disc_set_serials))
for (const auto& [title, glentry] :
GameList::GetEntriesInDiscSet(entry->disc_set, m_game_list_widget->getModel()->getShowLocalizedTitles()))
{
QAction* action = m_ui.menuChangeDisc->addAction(QtUtils::StringViewToQString(title));
QString path = QString::fromStdString(glentry->path);
@ -1562,11 +1563,14 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [qpath]() {
const auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(qpath.toStdString());
if (!entry)
if (!entry || !g_main_window)
return;
SettingsWindow::openGamePropertiesDialog(entry->path, std::string(entry->GetDisplayTitle()), entry->serial,
entry->hash, entry->region);
SettingsWindow::openGamePropertiesDialog(
entry->path,
std::string(
entry->GetDisplayTitle(g_main_window->m_game_list_widget->getModel()->getShowLocalizedTitles())),
entry->serial, entry->hash, entry->region);
});
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, qpath]() {
@ -1638,14 +1642,17 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
}
else
{
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [disc_set_name = qpath]() {
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [dsentry = entry->GetDiscSetEntry()]() {
// resolve path first
auto lock = GameList::GetLock();
const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(disc_set_name.toStdString());
if (first_disc)
const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(dsentry);
if (first_disc && g_main_window)
{
SettingsWindow::openGamePropertiesDialog(first_disc->path, std::string(first_disc->GetDisplayTitle()),
first_disc->serial, first_disc->hash, first_disc->region);
SettingsWindow::openGamePropertiesDialog(
first_disc->path,
std::string(
first_disc->GetDisplayTitle(g_main_window->m_game_list_widget->getModel()->getShowLocalizedTitles())),
first_disc->serial, first_disc->hash, first_disc->region);
}
});
@ -1743,7 +1750,8 @@ void MainWindow::clearGameListEntryPlayTime(const GameList::Entry* entry)
if (QMessageBox::question(
this, tr("Confirm Reset"),
tr("Are you sure you want to reset the play time for '%1'?\n\nThis action cannot be undone.")
.arg(QtUtils::StringViewToQString(entry->GetDisplayTitle()))) != QMessageBox::Yes)
.arg(QtUtils::StringViewToQString(
entry->GetDisplayTitle(m_game_list_widget->getModel()->getShowLocalizedTitles())))) != QMessageBox::Yes)
{
return;
}
@ -1779,7 +1787,8 @@ void MainWindow::setupAdditionalUi()
m_game_list_widget = new GameListWidget(m_ui.mainContainer);
m_game_list_widget->initialize(m_ui.actionViewGameList, m_ui.actionViewGameGrid, m_ui.actionMergeDiscSets,
m_ui.actionShowGameIcons, m_ui.actionGridViewShowTitles);
m_ui.actionShowGameIcons, m_ui.actionGridViewShowTitles,
m_ui.actionShowLocalizedTitles);
m_ui.mainContainer->addWidget(m_game_list_widget);
m_status_progress_widget = new QProgressBar(m_ui.statusBar);
@ -2364,6 +2373,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread,
&EmuThread::reloadTextureReplacements);
connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
connect(m_ui.actionShowLocalizedTitles, &QAction::triggered, m_game_list_widget,
&GameListWidget::setShowLocalizedTitles);
connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionGridViewZoomIn, &QAction::triggered, this, &MainWindow::onViewGameGridZoomInActionTriggered);

@ -30,7 +30,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
<height>33</height>
</rect>
</property>
<widget class="QMenu" name="menuSystem">
@ -212,6 +212,7 @@
<addaction name="menuWindowSize"/>
<addaction name="separator"/>
<addaction name="actionMergeDiscSets"/>
<addaction name="actionShowLocalizedTitles"/>
<addaction name="actionShowGameIcons"/>
<addaction name="actionGridViewShowTitles"/>
<addaction name="actionGridViewZoomIn"/>
@ -1056,6 +1057,17 @@
<string>Refresh Achievement &amp;Progress</string>
</property>
</action>
<action name="actionShowLocalizedTitles">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="language"/>
</property>
<property name="text">
<string>Show Localized Titles</string>
</property>
</action>
</widget>
<resources>
<include location="resources/duckstation-qt.qrc"/>

@ -81,6 +81,7 @@
<file>icons/black/svg/information-line.svg</file>
<file>icons/black/svg/joystick-line.svg</file>
<file>icons/black/svg/keyboard-line.svg</file>
<file>icons/black/svg/language.svg</file>
<file>icons/black/svg/layout-grid-line.svg</file>
<file>icons/black/svg/lightbulb-line.svg</file>
<file>icons/black/svg/list-check.svg</file>
@ -300,6 +301,7 @@
<file>icons/white/svg/information-line.svg</file>
<file>icons/white/svg/joystick-line.svg</file>
<file>icons/white/svg/keyboard-line.svg</file>
<file>icons/white/svg/language.svg</file>
<file>icons/white/svg/layout-grid-line.svg</file>
<file>icons/white/svg/lightbulb-line.svg</file>
<file>icons/white/svg/list-check.svg</file>

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-l</title><path d="M478.33,433.6l-90-218a22,22,0,0,0-40.67,0l-90,218a22,22,0,1,0,40.67,16.79L316.66,406H419.33l18.33,44.39A22,22,0,0,0,458,464a22,22,0,0,0,20.32-30.4ZM334.83,362,368,281.65,401.17,362Z"/><path d="M267.84,342.92a22,22,0,0,0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73,39.65-53.68,62.11-114.75,71.27-143.49H330a22,22,0,0,0,0-44H214V70a22,22,0,0,0-44,0V90H54a22,22,0,0,0,0,44H251.25c-9.52,26.95-27.05,69.5-53.79,108.36-31.41-41.68-43.08-68.65-43.17-68.87a22,22,0,0,0-40.58,17c.58,1.38,14.55,34.23,52.86,83.93.92,1.19,1.83,2.35,2.74,3.51-39.24,44.35-77.74,71.86-93.85,80.74a22,22,0,1,0,21.07,38.63c2.16-1.18,48.6-26.89,101.63-85.59,22.52,24.08,38,35.44,38.93,36.1a22,22,0,0,0,30.75-4.9Z"/></svg>

After

Width:  |  Height:  |  Size: 946 B

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-l</title><path d="M478.33,433.6l-90-218a22,22,0,0,0-40.67,0l-90,218a22,22,0,1,0,40.67,16.79L316.66,406H419.33l18.33,44.39A22,22,0,0,0,458,464a22,22,0,0,0,20.32-30.4ZM334.83,362,368,281.65,401.17,362Z"/><path d="M267.84,342.92a22,22,0,0,0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73,39.65-53.68,62.11-114.75,71.27-143.49H330a22,22,0,0,0,0-44H214V70a22,22,0,0,0-44,0V90H54a22,22,0,0,0,0,44H251.25c-9.52,26.95-27.05,69.5-53.79,108.36-31.41-41.68-43.08-68.65-43.17-68.87a22,22,0,0,0-40.58,17c.58,1.38,14.55,34.23,52.86,83.93.92,1.19,1.83,2.35,2.74,3.51-39.24,44.35-77.74,71.86-93.85,80.74a22,22,0,1,0,21.07,38.63c2.16-1.18,48.6-26.89,101.63-85.59,22.52,24.08,38,35.44,38.93,36.1a22,22,0,0,0,30.75-4.9Z"/></svg>

After

Width:  |  Height:  |  Size: 946 B

@ -13,11 +13,13 @@
#include "moc_selectdiscdialog.cpp"
SelectDiscDialog::SelectDiscDialog(const std::string& disc_set_name, QWidget* parent /* = nullptr */) : QDialog(parent)
SelectDiscDialog::SelectDiscDialog(const GameDatabase::DiscSetEntry* dsentry, bool localized_titles,
QWidget* parent /* = nullptr */)
: QDialog(parent)
{
m_ui.setupUi(this);
QtUtils::SetColumnWidthsForTreeView(m_ui.discList, {50, -1, 100});
populateList(disc_set_name);
populateList(dsentry, localized_titles);
updateStartEnabled();
connect(m_ui.select, &QPushButton::clicked, this, &SelectDiscDialog::onSelectClicked);
@ -57,10 +59,10 @@ void SelectDiscDialog::onCancelClicked()
reject();
}
void SelectDiscDialog::populateList(const std::string& disc_set_name)
void SelectDiscDialog::populateList(const GameDatabase::DiscSetEntry* dsentry, bool localized_titles)
{
const auto lock = GameList::GetLock();
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name);
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(dsentry, localized_titles);
const GameList::Entry* last_played_entry = nullptr;
for (const GameList::Entry* entry : entries)
@ -81,5 +83,6 @@ void SelectDiscDialog::populateList(const std::string& disc_set_name)
}
}
setWindowTitle(tr("Select Disc for %1").arg(QString::fromStdString(disc_set_name)));
setWindowTitle(
tr("Select Disc for %1").arg(QtUtils::StringViewToQString(dsentry->GetDisplayTitle(localized_titles))));
}

@ -9,12 +9,17 @@
#include <string>
namespace GameDatabase {
struct DiscSetEntry;
}
class SelectDiscDialog final : public QDialog
{
Q_OBJECT
public:
explicit SelectDiscDialog(const std::string& disc_set_name, QWidget* parent = nullptr);
explicit SelectDiscDialog(const GameDatabase::DiscSetEntry* dsentry, bool localized_titles,
QWidget* parent = nullptr);
~SelectDiscDialog();
ALWAYS_INLINE const std::string& getSelectedDiscPath() { return m_selected_path; }
@ -26,7 +31,7 @@ private Q_SLOTS:
void onCancelClicked();
private:
void populateList(const std::string& disc_set_name);
void populateList(const GameDatabase::DiscSetEntry* dsentry, bool localized_titles);
Ui::SelectDiscDialog m_ui;
std::string m_selected_path;

Loading…
Cancel
Save