GameList: Merge multi-disc games

This commit is contained in:
Stenzek
2024-05-18 15:16:54 +10:00
parent 9bdf23cba7
commit 1adaea9005
16 changed files with 706 additions and 114 deletions

View File

@ -464,6 +464,7 @@ 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 DrawGameListSettingsWindow();
static void SwitchToGameList();
static void PopulateGameListEntryList();
@ -5919,7 +5920,7 @@ bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* ent
void FullscreenUI::DrawResumeStateSelector()
{
ImGui::SetNextWindowSize(LayoutScale(800.0f, 600.0f));
ImGui::SetNextWindowSize(LayoutScale(800.0f, 602.0f));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(FSUI_CSTR("Load Resume State"));
@ -6048,11 +6049,27 @@ void FullscreenUI::PopulateGameListEntryList()
{
const s32 sort = Host::GetBaseIntSettingValue("Main", "FullscreenUIGameSort", 0);
const bool reverse = Host::GetBaseBoolSettingValue("Main", "FullscreenUIGameSortReverse", false);
const bool merge_disc_sets = Host::GetBaseBoolSettingValue("Main", "FullscreenUIMergeDiscSets", true);
const u32 count = GameList::GetEntryCount();
s_game_list_sorted_entries.resize(count);
s_game_list_sorted_entries.clear();
s_game_list_sorted_entries.reserve(count);
for (u32 i = 0; i < count; i++)
s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i);
{
const GameList::Entry* entry = GameList::GetEntryByIndex(i);
if (merge_disc_sets)
{
if (entry->disc_set_member)
continue;
}
else
{
if (entry->IsDiscSet())
continue;
}
s_game_list_sorted_entries.push_back(entry);
}
std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(),
[sort, reverse](const GameList::Entry* lhs, const GameList::Entry* rhs) {
@ -6539,6 +6556,12 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
{
if (entry->IsDiscSet())
{
HandleSelectDiscForDiscSet(entry->path);
return;
}
// launch game
if (!OpenLoadStateSelectorForGameResume(entry))
DoStartPath(entry->path);
@ -6546,53 +6569,121 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
{
ImGuiFullscreen::ChoiceDialogOptions options = {
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
{FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open Containing Directory"), false},
{FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false},
{FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false},
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Default Boot"), false},
{FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Fast Boot"), false},
{FSUI_ICONSTR(ICON_FA_MAGIC, "Slow Boot"), false},
{FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Reset Play Time"), false},
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
};
if (!entry->IsDiscSet())
{
ImGuiFullscreen::ChoiceDialogOptions options = {
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
{FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open Containing Directory"), false},
{FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false},
{FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false},
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Default Boot"), false},
{FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Fast Boot"), false},
{FSUI_ICONSTR(ICON_FA_MAGIC, "Slow Boot"), false},
{FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Reset Play Time"), false},
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
};
OpenChoiceDialog(
entry->title.c_str(), false, std::move(options),
[entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) {
switch (index)
{
case 0: // Open Game Properties
SwitchToGameSettingsForPath(entry_path);
break;
case 1: // Open Containing Directory
ExitFullscreenAndOpenURL(Path::CreateFileURL(Path::GetDirectory(entry_path)));
break;
case 2: // Resume Game
DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1));
break;
case 3: // Load State
OpenLoadStateSelectorForGame(entry_path);
break;
case 4: // Default Boot
DoStartPath(entry_path);
break;
case 5: // Fast Boot
DoStartPath(entry_path, {}, true);
break;
case 6: // Slow Boot
DoStartPath(entry_path, {}, false);
break;
case 7: // Reset Play Time
GameList::ClearPlayedTimeForSerial(entry_serial);
break;
default:
break;
}
OpenChoiceDialog(
entry->title.c_str(), false, std::move(options),
[entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) {
switch (index)
{
case 0: // Open Game Properties
SwitchToGameSettingsForPath(entry_path);
break;
case 1: // Open Containing Directory
ExitFullscreenAndOpenURL(Path::CreateFileURL(Path::GetDirectory(entry_path)));
break;
case 2: // Resume Game
DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1));
break;
case 3: // Load State
OpenLoadStateSelectorForGame(entry_path);
break;
case 4: // Default Boot
DoStartPath(entry_path);
break;
case 5: // Fast Boot
DoStartPath(entry_path, {}, true);
break;
case 6: // Slow Boot
DoStartPath(entry_path, {}, false);
break;
case 7: // Reset Play Time
GameList::ClearPlayedTimeForSerial(entry_serial);
break;
default:
break;
}
CloseChoiceDialog();
});
CloseChoiceDialog();
});
}
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_COMPACT_DISC, "Select Disc"), false},
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), 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) {
switch (index)
{
case 0: // Open Game Properties
SwitchToGameSettingsForPath(entry_path);
break;
case 1: // Select Disc
HandleSelectDiscForDiscSet(disc_set_name);
break;
default:
break;
}
CloseChoiceDialog();
});
}
}
void FullscreenUI::HandleSelectDiscForDiscSet(std::string_view disc_set_name)
{
auto lock = GameList::GetLock();
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name, true);
if (entries.empty())
return;
ImGuiFullscreen::ChoiceDialogOptions options;
std::vector<std::string> paths;
paths.reserve(entries.size());
for (const GameList::Entry* entry : entries)
{
std::string title = fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Disc {} | {}")),
entry->disc_set_index + 1, Path::GetFileName(entry->path));
options.emplace_back(std::move(title), false);
paths.push_back(entry->path);
}
options.emplace_back(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false);
OpenChoiceDialog(SmallString::from_format("Select Disc for {}", disc_set_name), false, std::move(options),
[paths = std::move(paths)](s32 index, const std::string& title, bool checked) {
if (static_cast<u32>(index) < paths.size())
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(paths[index]);
if (entry)
HandleGameListActivate(entry);
}
CloseChoiceDialog();
});
}
void FullscreenUI::DrawGameListSettingsWindow()
@ -6742,6 +6833,9 @@ void FullscreenUI::DrawGameListSettingsWindow()
bsi, FSUI_ICONSTR(ICON_FA_SORT_ALPHA_DOWN, "Sort Reversed"),
FSUI_CSTR("Reverses the game list sort order from the default (usually ascending to descending)."), "Main",
"FullscreenUIGameSortReverse", false);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST, "Merge Multi-Disc Games"),
FSUI_CSTR("Merges multi-disc games into one item in the game list."), "Main",
"FullscreenUIMergeDiscSets", true);
}
MenuHeading(FSUI_CSTR("Cover Settings"));

View File

@ -82,6 +82,7 @@ static bool OpenCacheForWriting();
static bool WriteEntryToCache(const Entry* entry);
static void CloseCacheFileStream();
static void DeleteCacheFile();
static void CreateDiscSetEntries(const PlayedTimeMap& played_time_map);
static std::string GetPlayedTimeFile();
static bool ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEntry& entry);
@ -100,15 +101,16 @@ static bool s_game_list_loaded = false;
const char* GameList::GetEntryTypeName(EntryType type)
{
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {{"Disc", "PSExe", "Playlist", "PSF"}};
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
{"Disc", "DiscSet", "PSExe", "Playlist", "PSF"}};
return names[static_cast<int>(type)];
}
const char* GameList::GetEntryTypeDisplayName(EntryType type)
{
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
{TRANSLATE_NOOP("GameList", "Disc"), TRANSLATE_NOOP("GameList", "PS-EXE"), TRANSLATE_NOOP("GameList", "Playlist"),
TRANSLATE_NOOP("GameList", "PSF")}};
{TRANSLATE_NOOP("GameList", "Disc"), TRANSLATE_NOOP("GameList", "Disc Set"), TRANSLATE_NOOP("GameList", "PS-EXE"),
TRANSLATE_NOOP("GameList", "Playlist"), TRANSLATE_NOOP("GameList", "PSF")}};
return Host::TranslateToCString("GameList", names[static_cast<int>(type)]);
}
@ -515,8 +517,8 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
}
std::unique_lock lock(s_mutex);
if (GetEntryForPath(ffd.FileName) ||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
if (GetEntryForPath(ffd.FileName) || AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) ||
only_cache)
{
continue;
}
@ -611,7 +613,7 @@ const GameList::Entry* GameList::GetEntryBySerial(std::string_view serial)
{
for (const Entry& entry : s_entries)
{
if (entry.serial == serial)
if (entry.serial == serial)
return &entry;
}
@ -629,19 +631,51 @@ 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(std::string_view disc_set_name,
bool sort_by_most_recent)
{
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 || disc_set_name != entry.disc_set_name)
continue;
ret.push_back(&entry);
}
if (sort_by_most_recent)
{
std::sort(ret.begin(), ret.end(), [](const Entry* lhs, const Entry* rhs) {
if (lhs->last_played_time == rhs->last_played_time)
return (lhs->disc_set_index < rhs->disc_set_index);
else
return (lhs->last_played_time > rhs->last_played_time);
});
}
else
{
std::sort(ret.begin(), ret.end(),
[](const Entry* lhs, const Entry* rhs) { return (lhs->disc_set_index < rhs->disc_set_index); });
}
return ret;
}
const GameList::Entry* GameList::GetFirstDiscSetMember(std::string_view disc_set_name)
{
for (const Entry& entry : s_entries)
{
if (!entry.disc_set_member || disc_set_name != entry.disc_set_name)
continue;
// Disc set should not have been created without the first disc being present.
if (entry.disc_set_index == 0)
return &entry;
}
return nullptr;
}
u32 GameList::GetEntryCount()
{
return static_cast<u32>(s_entries.size());
@ -703,6 +737,112 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
// don't need unused cache entries
CloseCacheFileStream();
s_cache_map.clear();
// merge multi-disc games
CreateDiscSetEntries(played_time);
}
void GameList::CreateDiscSetEntries(const PlayedTimeMap& played_time_map)
{
std::unique_lock lock(s_mutex);
for (size_t i = 0; i < s_entries.size(); i++)
{
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.c_str()))
continue;
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(entry.serial);
if (!dbentry)
continue;
// need at least two discs for a 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)
{
continue;
}
found_another_disc = true;
break;
}
if (!found_another_disc)
{
Log_DevFmt("Not creating disc set {}, only one disc found", disc_set_name);
continue;
}
Entry set_entry;
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
for (const std::string& set_serial : dbentry->disc_set_serials)
{
const auto it = played_time_map.find(set_serial);
if (it == played_time_map.end())
continue;
set_entry.last_played_time =
(set_entry.last_played_time == 0) ?
it->second.last_played_time :
((it->second.last_played_time != 0) ? std::min(set_entry.last_played_time, it->second.last_played_time) :
set_entry.last_played_time);
set_entry.total_played_time += it->second.total_played_time;
}
// mark all discs for this set as part of it, so we don't try to add them again, and for filtering
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)
{
continue;
}
Log_InfoFmt("Adding {} to disc set {}", other_entry.path, disc_set_name);
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;
set_entry.uncompressed_size += other_entry.uncompressed_size;
num_parts++;
}
Log_InfoFmt("Created disc set {} from {} entries", disc_set_name, num_parts);
// entry is done :)
s_entries.push_back(std::move(set_entry));
}
}
std::string GameList::GetCoverImagePathForEntry(const Entry* entry)
@ -959,9 +1099,23 @@ void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t las
Log_VerbosePrintf("Add %u seconds play time to %s -> now %u", static_cast<unsigned>(add_time), serial.c_str(),
static_cast<unsigned>(pt.total_played_time));
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(serial);
std::unique_lock<std::recursive_mutex> lock(s_mutex);
for (GameList::Entry& entry : s_entries)
{
// add it to the disc set, if any
if (entry.type == EntryType::DiscSet)
{
if (dbentry && dbentry->disc_set_name == entry.path)
{
entry.last_played_time = pt.last_played_time;
entry.total_played_time = pt.total_played_time;
}
continue;
}
if (entry.serial != serial)
continue;

View File

@ -25,6 +25,7 @@ namespace GameList {
enum class EntryType
{
Disc,
DiscSet,
PSExe,
Playlist,
PSF,
@ -57,12 +58,14 @@ struct Entry
u8 min_blocks = 0;
u8 max_blocks = 0;
s8 disc_set_index = -1;
bool disc_set_member = false;
GameDatabase::CompatibilityRating compatibility = GameDatabase::CompatibilityRating::Unknown;
size_t GetReleaseDateString(char* buffer, size_t buffer_size) const;
ALWAYS_INLINE bool IsDisc() const { return (type == EntryType::Disc); }
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
};
const char* GetEntryTypeName(EntryType type);
@ -80,7 +83,8 @@ 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);
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);
u32 GetEntryCount();
bool IsGameListLoaded();