From ec16540c119505c64968da1a7e86d41dd0898f40 Mon Sep 17 00:00:00 2001 From: gugueU Date: Fri, 4 Apr 2025 22:36:47 +0200 Subject: [PATCH] feat(frontend): add one game one rom filter --- RELEASE-NOTES.md | 5 +- projects/frontend/data/resources/menu.xml | 4 + .../frontend/es-app/src/games/FileSorts.cpp | 6 +- .../es-app/src/games/MetadataDescriptor.h | 8 + .../es-app/src/guis/menus/MenuBuilder.cpp | 6 +- .../src/guis/menus/MenuDataProvider.cpp | 2 +- .../es-app/src/guis/menus/MenuDataProvider.h | 2 +- .../es-app/src/guis/menus/MenuItemType.cpp | 3 + .../es-app/src/guis/menus/MenuItemType.h | 3 + .../es-app/src/guis/menus/MenuProvider.cpp | 9 + .../views/gamelist/DetailedGameListView.cpp | 268 +++++++++++++++++- .../src/views/gamelist/DetailedGameListView.h | 62 +++- projects/frontend/es-core/src/RecalboxConf.h | 6 + 13 files changed, 364 insertions(+), 20 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4014bee22f..6e39fc48a9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -27,9 +27,8 @@ as release notes for end user on a Recalbox upgrade. - Add Tamagotchi (tamalibretro core) - Add NetPlay support for cores libretro-gpsp and libretro-dosbox-pure - Add original xbox emulator (xemu) -- Add new aliases and licences -- Add new search aliases or licences -- Add new sort groupy by alias +- Add new search by aliases or licences +- Add new One Game One Rom filter - Add new Atari core, HatariB set as new default emulator for Atari ST(E)/Mega ST(E)/TT/Falcon - Add AppleWin libretro core in Apple II system - Add libretro dolphin diff --git a/projects/frontend/data/resources/menu.xml b/projects/frontend/data/resources/menu.xml index f8f2cda0db..ec6e1129c9 100644 --- a/projects/frontend/data/resources/menu.xml +++ b/projects/frontend/data/resources/menu.xml @@ -247,6 +247,10 @@ + + + +
diff --git a/projects/frontend/es-app/src/games/FileSorts.cpp b/projects/frontend/es-app/src/games/FileSorts.cpp index 5d16b4c39e..0274ec2213 100644 --- a/projects/frontend/es-app/src/games/FileSorts.cpp +++ b/projects/frontend/es-app/src/games/FileSorts.cpp @@ -78,8 +78,8 @@ int FileSorts::compareFoldersAndGames(const FileData& fd1, const FileData& fd2) { if (sUseTopFavorites && f1 == ItemType::Game) { - bool fv1 = fd1.Metadata().Favorite(); - bool fv2 = fd2.Metadata().Favorite(); + bool fv1 = fd1.Metadata().Favorite() || fd1.Metadata().HasFavoriteChild(); + bool fv2 = fd2.Metadata().Favorite() || fd2.Metadata().HasFavoriteChild(); if (fv1 == fv2) return 0; // Both are favorites or not if (fv1) return -1; // fv1 is favorite, fv2 is not return 1; // fv2 is favorite, fv1 is not @@ -168,7 +168,7 @@ ImplementSortMethod(compareDevelopper) CheckFoldersAndGames(file1, file2) int result = unicodeCompareUppercase(file1.Metadata().Developer().Trim(), file2.Metadata().Developer().Trim()); if (result != 0) return result; - return compareFileName(file1, file2); + return compareFileName(file1, file2); } ImplementSortMethod(comparePublisher) diff --git a/projects/frontend/es-app/src/games/MetadataDescriptor.h b/projects/frontend/es-app/src/games/MetadataDescriptor.h index 3c729af7b7..38744bb145 100755 --- a/projects/frontend/es-app/src/games/MetadataDescriptor.h +++ b/projects/frontend/es-app/src/games/MetadataDescriptor.h @@ -101,6 +101,7 @@ class MetadataDescriptor bool mPreinstalled:1; //!< Preinstalled game? bool mNoGame:1; //!< Nor a game (application, tools, ...) bool mDirty:1; //!< Dirty flag (modified data flag) + bool mHasFavoriteChild:1; int mLightgunLuminosity:4; //!< LightGunLuminosity. First bit is to check if default, last 3 bits is the value : 0000 = default 0, 1000 = 0 forced bool mPathValidated:1;//!< File path has been validated during loading @@ -237,6 +238,7 @@ class MetadataDescriptor , mPreinstalled(false) , mNoGame(false) , mDirty(false) + , mHasFavoriteChild(false) , mLightgunLuminosity(0) , mPathValidated(false) , mDirtyEnabled(false) @@ -290,6 +292,8 @@ class MetadataDescriptor mType(source.mType), mRotation(source.mRotation), mTimePlayed(source.mTimePlayed), + mHasFavoriteChild(false) + { LivingClasses++; if (_Type == ItemType::Game) LivingGames++; @@ -506,6 +510,8 @@ class MetadataDescriptor [[nodiscard]] int SupportIndex() const { return mSupportIndex; } [[nodiscard]] int SupportTotal() const { return mSupportTotal; } [[nodiscard]] SupportSides SupportSide() const { return mSupportSide; } + [[nodiscard]] bool HasFavoriteChild()const { return mHasFavoriteChild; } + [[nodiscard]] int LightgunLuminosity() const { return mLightgunLuminosity & 0x3; } [[nodiscard]] SupportTypes SupportType() const { return mSupportType; } @@ -618,6 +624,8 @@ class MetadataDescriptor void SetSupportIndex(int index) { mSupportIndex = index; } void SetSupportTotal(int total) { mSupportTotal = total; } void SetSupportSide(SupportSides side) { mSupportSide = side; } + void SetHasFavoriteChild(bool hasFavoriteChild) { mHasFavoriteChild = hasFavoriteChild; } + void SetSupportType(SupportTypes type) { mSupportType = type; } //! Disable dirtiness until EnableDirtiness is called diff --git a/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp b/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp index 9fc03704d9..0b4dd5770f 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp +++ b/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp @@ -553,7 +553,8 @@ void MenuBuilder::AddItem(const InheritableContext& context, const ItemDefinitio case MenuItemType::ScrapeOptionVideo: { AddList(context, item, MenuDataProvider::GetScraperVideosEntries(), mConf.GetScreenScraperVideo(), ScreenScraperEnums::ScreenScraperVideoType::None, defaultGrayed); break; } case MenuItemType::ScrapeOptionThumb: { AddList(context, item, MenuDataProvider::GetScraperThumbnailsEntries(), mConf.GetScreenScraperThumbnail(), ScreenScraperEnums::ScreenScraperImageType::None, defaultGrayed); break; } case MenuItemType::ScrapeOptionRegionPriority: { AddList(context, item, MenuDataProvider::GetScraperRegionOptionsEntries(), mConf.GetScreenScraperRegionPriority(), ScreenScraperEnums::ScreenScraperRegionPriority::DetectedRegion, defaultGrayed); break; } - case MenuItemType::ScrapeOptionRegionFavorite: { AddList(context, item, MenuDataProvider::GetScraperRegionsEntries(), mConf.GetScreenScraperRegion(), Regions::GameRegions::WOR, defaultGrayed); break; } + case MenuItemType::ScrapeOptionRegionFavorite: { AddList(context, item, + MenuDataProvider::GetRegionEntries(), mConf.GetScreenScraperRegion(), Regions::GameRegions::WOR, defaultGrayed); break; } case MenuItemType::ScrapeOptionLanguageFavorite: { AddList(context, item, MenuDataProvider::GetScraperLanguagesEntries(), LanguagesTools::GetScrapingLanguage(), Languages::EN, defaultGrayed); break; } case MenuItemType::ScrapeOptionManual: { AddSwitch(context, item, mConf.GetScreenScraperWantManual(), defaultGrayed); break; } case MenuItemType::ScrapeOptionMap: { AddSwitch(context, item, mConf.GetScreenScraperWantMaps(), defaultGrayed); break; } @@ -643,6 +644,9 @@ void MenuBuilder::AddItem(const InheritableContext& context, const ItemDefinitio case MenuItemType::OnScreenHelp: { AddSwitch(context, item, mConf.GetShowHelp(), defaultGrayed); break; } case MenuItemType::SwapValidateCancel: { AddSwitch(context, item, mConf.GetSwapValidateAndCancel(), defaultGrayed); break; } case MenuItemType::OSDClock: { AddSwitch(context, item, mConf.GetClock(), defaultGrayed); break; } + case MenuItemType::OneGameOneRom: { AddSwitch(context, item, mConf.GetOneGameOneRom(), defaultGrayed); break; } + case MenuItemType::RomPreferredRegion: { AddList(context, item, MenuDataProvider::GetRegionEntries(), Regions::Clamp(mConf.GetRomPreferredRegion()), Regions::GameRegions::EU, defaultGrayed); break; } + case MenuItemType::RomSecondPreferredRegion: { AddList(context, item, MenuDataProvider::GetRegionEntries(), Regions::Clamp(mConf.GetRomSecondPreferredRegion()), Regions::GameRegions::EU, defaultGrayed); break; } case MenuItemType::DisplayByFilename: { AddSwitch(context, item, mConf.GetDisplayByFileName(), defaultGrayed); break; } case MenuItemType::UpdateGamelists: { AddAction(context, item, true, defaultGrayed); break; } case MenuItemType::ShowSystems: { AddMultiList(context, item, mDataProvider.GetSystemShown(), defaultGrayed); break; } diff --git a/projects/frontend/es-app/src/guis/menus/MenuDataProvider.cpp b/projects/frontend/es-app/src/guis/menus/MenuDataProvider.cpp index 8395fd3ac9..1e24c8030d 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuDataProvider.cpp +++ b/projects/frontend/es-app/src/guis/menus/MenuDataProvider.cpp @@ -201,7 +201,7 @@ SelectorEntry::List MenuDataPro return list; } -SelectorEntry::List MenuDataProvider::GetScraperRegionsEntries() +SelectorEntry::List MenuDataProvider::GetRegionEntries() { SelectorEntry::List list; for(Regions::GameRegions region : Regions::AvailableRegions()) diff --git a/projects/frontend/es-app/src/guis/menus/MenuDataProvider.h b/projects/frontend/es-app/src/guis/menus/MenuDataProvider.h index 722defacb5..bf64f690dc 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuDataProvider.h +++ b/projects/frontend/es-app/src/guis/menus/MenuDataProvider.h @@ -59,7 +59,7 @@ class MenuDataProvider //! Get scraped region priority static SelectorEntry::List GetScraperRegionOptionsEntries(); //! Get favorite region when scrping - static SelectorEntry::List GetScraperRegionsEntries(); + static SelectorEntry::List GetRegionEntries(); //! Get available scraped languages static SelectorEntry::List GetScraperLanguagesEntries(); //! Get Softpatching option list diff --git a/projects/frontend/es-app/src/guis/menus/MenuItemType.cpp b/projects/frontend/es-app/src/guis/menus/MenuItemType.cpp index b4fb4e0443..4566d724e2 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuItemType.cpp +++ b/projects/frontend/es-app/src/guis/menus/MenuItemType.cpp @@ -163,6 +163,9 @@ static const HashMap sStringToItemType { "OnScreenHelp", MenuItemType::OnScreenHelp }, { "SwapValidateCancel", MenuItemType::SwapValidateCancel }, { "OSDClock", MenuItemType::OSDClock }, + { "OneGameOneRom", MenuItemType::OneGameOneRom }, + { "RomPreferredRegion", MenuItemType::RomPreferredRegion }, + { "RomSecondPreferredRegion", MenuItemType::RomSecondPreferredRegion }, { "DisplayByFilename", MenuItemType::DisplayByFilename }, { "UpdateGamelists", MenuItemType::UpdateGamelists }, { "ShowSystems", MenuItemType::ShowSystems }, diff --git a/projects/frontend/es-app/src/guis/menus/MenuItemType.h b/projects/frontend/es-app/src/guis/menus/MenuItemType.h index 1d4f62362c..f16acf4ca5 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuItemType.h +++ b/projects/frontend/es-app/src/guis/menus/MenuItemType.h @@ -163,6 +163,9 @@ enum class MenuItemType OnScreenHelp, SwapValidateCancel, OSDClock, + OneGameOneRom, + RomPreferredRegion, + RomSecondPreferredRegion, DisplayByFilename, UpdateGamelists, ShowSystems, diff --git a/projects/frontend/es-app/src/guis/menus/MenuProvider.cpp b/projects/frontend/es-app/src/guis/menus/MenuProvider.cpp index df2f14b2c3..5cfe463be3 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuProvider.cpp +++ b/projects/frontend/es-app/src/guis/menus/MenuProvider.cpp @@ -666,6 +666,13 @@ void MenuProvider::MenuSwitchChanged(const ItemSwitch& item, bool& value, int id case MenuItemType::OnScreenHelp: mConf.SetShowHelp(value).Save(); break; case MenuItemType::SwapValidateCancel: mConf.SetSwapValidateAndCancel(value).Save(); mWindow.UpdateHelpSystem(); break; case MenuItemType::OSDClock: mConf.SetClock(value).Save(); break; + case MenuItemType::OneGameOneRom: + { + mConf.SetOneGameOneRom(value).Save(); + ViewController::Instance().GetOrCreateGamelistView(ViewController::Instance().CurrentSystem())->refreshList(); + ViewController::Instance().InvalidateAllGamelistsExcept(nullptr); + break; + } case MenuItemType::DisplayByFilename: { mConf.SetDisplayByFileName(value).Save(); @@ -942,6 +949,8 @@ void MenuProvider::MenuSingleChanged(ItemSelectorBase& ite switch((MenuItemType)id) { case MenuItemType::ScrapeOptionRegionFavorite: mConf.SetScreenScraperRegion(value).Save(); break; + case MenuItemType::RomPreferredRegion: mConf.SetRomPreferredRegion(value).Save(); break; + case MenuItemType::RomSecondPreferredRegion: mConf.SetRomSecondPreferredRegion(value).Save(); break; case MenuItemType::HighlightRegion: assert(item.Context().HasSystem() && "No system context"); mConf.SetSystemRegionFilter(*item.Context().System(), value).Save(); break; default: mWindow.displayMessage("[MenuProcessor] Unprocessed id " + MenuConverters::ItemToString((MenuItemType)id) + " in MenuSingleChanged !"); diff --git a/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.cpp b/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.cpp index e88daa2a97..5e1d10c807 100755 --- a/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -1179,6 +1179,21 @@ void DetailedGameListView::populateList(const FolderData& folder) FolderData::Sort(items, sorts.ComparerFromSort(sort), sorts.IsAscending(sort)); mSort = sort; + // loop twice for set HasOtherVersions flag + // use for sort by alias. if alone sort by name, otherwise sort by alias + Regions::GameRegions firstPreferredRegion = Regions::Clamp(RecalboxConf::Instance().GetRomPreferredRegion()); + Regions::GameRegions secondPreferredRegion = Regions::Clamp(RecalboxConf::Instance().GetRomSecondPreferredRegion()); + + bool isOneGameOneRomMode = !IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom(); + if (isOneGameOneRomMode) + { + // compute one game on rom structs parent and children and filter only parent items + items = GeneratePreferredOneGameOneRoms(items, firstPreferredRegion, secondPreferredRegion, sorts); + + } + // sort them + FolderData::Sort(items, sorts.ComparerFromSort(sort), sorts.IsAscending(sort)); + // Region filtering? Regions::GameRegions currentRegion = Regions::Clamp(RecalboxConf::Instance().GetSystemRegionFilter(mSystem)); bool activeRegionFiltering = false; @@ -1212,17 +1227,17 @@ void DetailedGameListView::populateList(const FolderData& folder) { // Has any favorite? for(int t = items.Count() - 1, i = (items.Count() + 1) / 2; --i >= 0; ) - if (items[i]->Metadata().Favorite() || items[t - i]->Metadata().Favorite()) { hasTopFavorites = true; break; } + if (ItemIsFavorite(items[i], isOneGameOneRomMode) || ItemIsFavorite(items[t - i], isOneGameOneRomMode)) { hasTopFavorites = true; break; } // If we have favorite + favorite first + descending order, move favorites on top if (hasTopFavorites && !sorts.IsAscending(sort)) { int favoriteCount = 0; int favoriteBase = 0; for(int i = items.Count(); --i >= 0; ) - if (items[i]->Metadata().Favorite()) + if (ItemIsFavorite(items[i], isOneGameOneRomMode)) { for(int j = i + 1; --j >= 0; ) - if (items[j]->Metadata().Favorite()) { favoriteCount++; favoriteBase = j; } + if (ItemIsFavorite(items[j], isOneGameOneRomMode)) { favoriteCount++; favoriteBase = j; } else break; break; } @@ -1232,6 +1247,7 @@ void DetailedGameListView::populateList(const FolderData& folder) for (FileData* item : items) { + String name = item->Name(); // Region filtering? int colorIndexOffset = BaseColor; if (activeRegionFiltering) @@ -1243,14 +1259,33 @@ void DetailedGameListView::populateList(const FolderData& folder) if (onlyYoko && RotationUtils::IsTate(item->Metadata().Rotation())) continue; // Header? if (item->IsGame()) - if (HeaderData* header = NeedHeader(previous, item, hasTopFavorites); header != nullptr) + if (HeaderData* header = NeedHeader(previous, item, hasTopFavorites, isOneGameOneRomMode); header != nullptr) { mList.add(header->Name(leftIcon), header, GameColor, HeaderColor, headerAlignment); lastHeader = header; } // Store if (lastHeader == nullptr || !lastHeader->IsFolded()) - mList.add(GetDisplayName(*item), item, colorIndexOffset + (item->IsFolder() ? FolderColor : GameColor), false); + { + if (isOneGameOneRomMode) + { + // get children if exist and display them if unfolded + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias[item->Metadata().MainAliasOrName()]; + bool isFolded = oneGameOneRom->mFolded; + String itemName = isFolded ? "▶ " + GetDisplayName(*item) : "▼ " +GetDisplayName(*item); + + mList.add(oneGameOneRom->mChildren.Count() > 0 ? itemName : GetDisplayName(*item), item, colorIndexOffset + (item->IsFolder() ? FolderColor : GameColor), false); + + if (!isFolded) + for (auto& child: oneGameOneRom->mChildren) + { + mList.add(" \u2022 " + GetDisplayName(*child), child, colorIndexOffset + GameColor, false); + } + } + else + mList.add(GetDisplayName(*item), item, colorIndexOffset + (item->IsFolder() ? FolderColor : GameColor), false); + } + if (item->IsGame()) previous = item; } @@ -1293,6 +1328,16 @@ void DetailedGameListView::setCursor(FileData* cursor) { if(!mList.setCursor(cursor, 0)) { + if (cursor->IsGame() && !IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom()) + { + String alias = cursor->Metadata().MainAliasOrName(); + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias[alias]; + if (cursor != oneGameOneRom->mParent) + { + oneGameOneRom->mFolded = false; + } + } + populateList(mSystem.MasterRoot()); mList.setCursor(cursor); @@ -1445,19 +1490,19 @@ void DetailedGameListView::BuildVideoLinks(const ThemeData& theme) } } -HeaderData* DetailedGameListView::NeedHeader(FileData* previous, FileData* next, bool hasTopFavorites) +HeaderData* DetailedGameListView::NeedHeader(FileData* previous, FileData* next, bool hasTopFavorites, bool isOneGameOneRom) { // Favorites if (hasTopFavorites) { - if (next->Metadata().Favorite()) + if (ItemIsFavorite(next, isOneGameOneRom)) { - if (previous == nullptr || (!previous->Metadata().Favorite() && next->Metadata().Favorite())) + if (previous == nullptr || (!ItemIsFavorite(previous, isOneGameOneRom) && ItemIsFavorite(next, isOneGameOneRom))) return GetHeader(_("Favorites")); return nullptr; } // Leaving favorite zone: reset prevous has if it is the first encountered game - if (previous != nullptr && previous->Metadata().Favorite()) + if (previous != nullptr && ItemIsFavorite(previous, isOneGameOneRom)) previous = nullptr; } @@ -1470,7 +1515,7 @@ HeaderData* DetailedGameListView::NeedHeader(FileData* previous, FileData* next, if (hasTopFavorites) { // Leavinf favorite area - if ((previous != nullptr && previous->Metadata().Favorite() && !next->Metadata().Favorite()) || + if ((previous != nullptr && ItemIsFavorite(previous, isOneGameOneRom) && !ItemIsFavorite(next, isOneGameOneRom)) || // List has favorites but not at top (previous == nullptr)) return GetHeader(_("In alphabetical order")); @@ -1680,6 +1725,48 @@ bool DetailedGameListView::ProcessInput(const InputCompactEvent& event) { if (event.HotkeyL2Released()) { ChangeSort(false); return true; } if (event.HotkeyR2Released()) { ChangeSort(true); return true; } + + FileData* item = getCursor(); + bool vertical = event.HotkeyDownReleased() || event.HotkeyUpReleased(); + if (item->IsGame() && !IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom()) + { + if (event.HotkeyLeftReleased() && !vertical) + { + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias[item->Metadata().MainAliasOrName()]; + oneGameOneRom->mFolded = true; + populateList(*mPopulatedFolder); + setCursor(oneGameOneRom->mParent); + return true; + } + if (event.HotkeyRightReleased() && !vertical) + { + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias[item->Metadata().MainAliasOrName()]; + oneGameOneRom->mFolded = false; + populateList(*mPopulatedFolder); + setCursor(item); + return true; + } + } + if (event.HotkeyUpReleased() && !IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom()) + { + for (auto& alias : mAliases) + { + mOneGameOnRomByAlias[alias]->mFolded = true; + } + populateList(*mPopulatedFolder); + setCursor(item); + return true; + } + if (event.HotkeyDownReleased() && !IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom()) + { + for (auto& alias : mAliases) + { + mOneGameOnRomByAlias[alias]->mFolded = false; + } + populateList(*mPopulatedFolder); + setCursor(item); + return true; + } } return ISimpleGameListView::ProcessInput(event); @@ -1874,3 +1961,164 @@ void DetailedGameListView::RemoveGame(FileData& game) if (mList.remove(&game)) ListRefreshRequired(); } + +FileData::List DetailedGameListView::GeneratePreferredOneGameOneRoms(const FileData::List& items, Regions::GameRegions firstPreferredRegion, Regions::GameRegions secondPreferredRegion, FileSorts& sorts) +{ + FileData::List filteredGames; + + FileData::List parents; + HashMap gamesByAlias; + mAliases.clear(); + for (FileData* item: items) + { + const String alias = item->Metadata().MainAliasOrName(); + gamesByAlias[alias].Add(item); + mAliases.insert(alias); + } + + for (auto& alias : mAliases) + { + FileData::List byAlias = gamesByAlias.get_or_return_default(alias); + bool found = false; + FileData* parent; + + // choose with region 1rst favorite region and is latestversion + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, firstPreferredRegion) && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + } + + // choose with region 1rst favorite region if not latest version + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, firstPreferredRegion) && game->Metadata().LatestVersion() && !found) + { + parent = game; + found = true; + break; + } + } + + // choose with region 2nd favorite region and if latest version + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, secondPreferredRegion) && game->Metadata().LatestVersion() && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + } + + // choose with region 2nd favorite region if not latest version + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, secondPreferredRegion) && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + } + + // choose with region WOR + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, Regions::GameRegions::WOR) && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + } + + // choose with region US + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, Regions::GameRegions::US) && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + } + + // choose with region JP + for (auto& game: byAlias) + { + if (Regions::IsIn4Regions(game->Metadata().Region().Pack, Regions::GameRegions::JP) && !found) + { + parent = game; + parents.Add(parent); + found = true; + break; + } + + // choose fisrt one.. + if (!found) + { + parent = byAlias[0]; + parents.Add(parent); + found = true; + } + + } + + // change with another way to pick game + if (!found) parent = byAlias[0]; + + FileData::List children; + for (auto& game : gamesByAlias[alias]) + { + if (game == parent) + continue; + + children.Add(game); + } + + bool hasFavoriteChild = false; + for (auto& child : children) + if (child->Metadata().Favorite()) + { + hasFavoriteChild = true; + break; + } + parent->Metadata().SetHasFavoriteChild(hasFavoriteChild); + + + FileSorts::Sorts sort = FileSorts::Sorts::RegionAscending; + FolderData::Sort(children, sorts.ComparerFromSort(sort), sorts.IsAscending(sort)); + + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias.get_or_return_default(alias); + if (oneGameOneRom == nullptr) + { + oneGameOneRom = new OneGameOneRom(parent, children); + mOneGameOnRomByAlias[alias] = oneGameOneRom; + } + else + { + oneGameOneRom->mParent = parent; + oneGameOneRom->mChildren = children; + } + } + + return parents; +} + +bool DetailedGameListView::ItemIsFavorite(FileData* game, bool isOneGameOneRom) +{ + if (!isOneGameOneRom) return game->Metadata().Favorite(); + + OneGameOneRom* oneGameOneRom = mOneGameOnRomByAlias.get_or_return_default(game->Metadata().MainAliasOrName()); + return oneGameOneRom->HasFavorite(); +} diff --git a/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.h b/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.h index baa01903b8..2ad0fb727e 100644 --- a/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.h +++ b/projects/frontend/es-app/src/views/gamelist/DetailedGameListView.h @@ -13,6 +13,36 @@ #include #include +#include + +struct OneGameOneRom +{ + FileData* mParent; + FileData::List mChildren; + bool mFolded; //!< True if the parent is folded + + OneGameOneRom(FileData* parent, FileData::List children) : + mParent(parent), + mChildren(std::move(children)), + mFolded(true) + {} + + bool HasFavorite() + { + if (mParent->Metadata().Favorite()) + return true; + + for (auto& child : mChildren) + { + if (child->Metadata().Favorite()) + return true; + } + + return false; + } + +}; + class DetailedGameListView : public ISimpleGameListView , public ITextListComponentOverlay , private IScraperEngineStage @@ -20,6 +50,8 @@ class DetailedGameListView : public ISimpleGameListView , private RecalboxConf::ISystemGamelistDecorationNotification , private RecalboxConf::ISystemSortNotification , private RecalboxConf::ISystemRegionFilterNotification + , private RecalboxConf::IRomPreferredRegionNotification + , private RecalboxConf::IRomSecondPreferredRegionNotification , private RecalboxConf::ISystemFlatFoldersNotification , private RecalboxConf::IShowOnlyLatestVersionNotification , private RecalboxConf::IFavoritesOnlyNotification @@ -63,6 +95,13 @@ class DetailedGameListView : public ISimpleGameListView int index = getCursorIndex(); // Refresh list populateList(*mPopulatedFolder); + + // if 1G1R + if (!IsFavoriteSystem() && RecalboxConf::Instance().GetOneGameOneRom()) + { + setCursor(currentItem); + } + // Try to set the old data. If it does not exist anymore, ajust index if (!mList.setCursor(currentItem)) setCursorIndex(index); @@ -236,6 +275,10 @@ class DetailedGameListView : public ISimpleGameListView //! Current sort FileSorts::Sorts mSort; + HashMap mGamesByAlias; + HashSet mAliases; + HashMap mOneGameOnRomByAlias; + //! Current gamelist decorations RecalboxConf::GamelistDecoration mDecorations; @@ -422,7 +465,7 @@ class DetailedGameListView : public ISimpleGameListView * @param hasTopFavorites instruct the method that the list has at least one favorite on top or bottom * @return No null header pointer if a header is required, nullptr otherwise */ - HeaderData* NeedHeader(FileData* previous, FileData* next, bool hasTopFavorites); + HeaderData* NeedHeader(FileData* previous, FileData* next, bool hasTopFavorites, bool isOneGameOneRom); /*! * @brief Create or get a named header @@ -564,6 +607,19 @@ class DetailedGameListView : public ISimpleGameListView void ConfigurationSystemRegionFilterChanged(const SystemData& system, const Regions::GameRegions& state) final { (void)state; RefreshRequiredIfSelf(system); } + /* + * RecalboxConf::RomPreferredRegion implementation + */ + + void ConfigurationRomPreferredRegionChanged(const Regions::GameRegions& state) final { (void)state; ListRefreshRequired(); } + + /* + * RecalboxConf::RomSecondPreferredRegion implementation + */ + + void ConfigurationRomSecondPreferredRegionChanged(const Regions::GameRegions& state) final { (void)state; ListRefreshRequired(); } + + /* * RecalboxConf::SystemFlatFoldersNotify implementation */ @@ -635,4 +691,8 @@ class DetailedGameListView : public ISimpleGameListView */ void ConfigurationHideNoGamesChanged(const bool& value) final { (void)value; ListRefreshRequired(); } + + FileData::List GeneratePreferredOneGameOneRoms(const FileData::List& items, Regions::GameRegions firstPreferredRegion, Regions::GameRegions secondPreferredRegion, FileSorts& sorts); + + bool ItemIsFavorite(FileData* game, bool isOneGameOneRom); }; diff --git a/projects/frontend/es-core/src/RecalboxConf.h b/projects/frontend/es-core/src/RecalboxConf.h index 54b6897bef..bb997a836a 100644 --- a/projects/frontend/es-core/src/RecalboxConf.h +++ b/projects/frontend/es-core/src/RecalboxConf.h @@ -255,6 +255,8 @@ class RecalboxConf: public IniFile, public StaticLifeCycleControler