diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index 7132e5d405..848b5c4970 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -30,7 +30,7 @@ import java.util.Set; public final class FileBrowserHelper { public static final HashSet GAME_EXTENSIONS = new HashSet<>(Arrays.asList( - "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf")); + "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf", "json")); public static final HashSet GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS); diff --git a/Source/Core/DiscIO/Blob.cpp b/Source/Core/DiscIO/Blob.cpp index 4eac308a27..7062c363cd 100644 --- a/Source/Core/DiscIO/Blob.cpp +++ b/Source/Core/DiscIO/Blob.cpp @@ -50,6 +50,8 @@ std::string GetName(BlobType blob_type, bool translate) return "WIA"; case BlobType::RVZ: return "RVZ"; + case BlobType::MOD_DESCRIPTOR: + return translate_str("Mod"); default: return ""; } diff --git a/Source/Core/DiscIO/Blob.h b/Source/Core/DiscIO/Blob.h index 3d593049f9..0a7cf09664 100644 --- a/Source/Core/DiscIO/Blob.h +++ b/Source/Core/DiscIO/Blob.h @@ -39,6 +39,7 @@ enum class BlobType TGC, WIA, RVZ, + MOD_DESCRIPTOR, }; std::string GetName(BlobType blob_type, bool translate); diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index 85b09600bb..a8c62fbd2e 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -352,16 +352,17 @@ void GameList::ShowContextMenu(const QPoint&) else { const auto game = GetSelectedGame(); + const bool is_mod_descriptor = game->IsModDescriptor(); DiscIO::Platform platform = game->GetPlatform(); menu->addAction(tr("&Properties"), this, &GameList::OpenProperties); - if (platform != DiscIO::Platform::ELFOrDOL) + if (!is_mod_descriptor && platform != DiscIO::Platform::ELFOrDOL) { menu->addAction(tr("&Wiki"), this, &GameList::OpenWiki); } menu->addSeparator(); - if (DiscIO::IsDisc(platform)) + if (!is_mod_descriptor && DiscIO::IsDisc(platform)) { menu->addAction(tr("Start with Riivolution Patches..."), this, &GameList::StartWithRiivolution); @@ -382,7 +383,7 @@ void GameList::ShowContextMenu(const QPoint&) menu->addSeparator(); } - if (platform == DiscIO::Platform::WiiDisc) + if (!is_mod_descriptor && platform == DiscIO::Platform::WiiDisc) { auto* perform_disc_update = menu->addAction(tr("Perform System Update"), this, [this, file_path = game->GetFilePath()] { @@ -394,7 +395,7 @@ void GameList::ShowContextMenu(const QPoint&) perform_disc_update->setEnabled(!Core::IsRunning() || !SConfig::GetInstance().bWii); } - if (platform == DiscIO::Platform::WiiWAD) + if (!is_mod_descriptor && platform == DiscIO::Platform::WiiWAD) { QAction* wad_install_action = new QAction(tr("Install to the NAND"), menu); QAction* wad_uninstall_action = new QAction(tr("Uninstall from the NAND"), menu); @@ -420,14 +421,15 @@ void GameList::ShowContextMenu(const QPoint&) menu->addSeparator(); } - if (platform == DiscIO::Platform::WiiWAD || platform == DiscIO::Platform::WiiDisc) + if (!is_mod_descriptor && + (platform == DiscIO::Platform::WiiWAD || platform == DiscIO::Platform::WiiDisc)) { menu->addAction(tr("Open Wii &Save Folder"), this, &GameList::OpenWiiSaveFolder); menu->addAction(tr("Export Wii Save"), this, &GameList::ExportWiiSave); menu->addSeparator(); } - if (platform == DiscIO::Platform::GameCubeDisc) + if (!is_mod_descriptor && platform == DiscIO::Platform::GameCubeDisc) { menu->addAction(tr("Open GameCube &Save Folder"), this, &GameList::OpenGCSaveFolder); menu->addSeparator(); diff --git a/Source/Core/DolphinQt/GameList/GameTracker.cpp b/Source/Core/DolphinQt/GameList/GameTracker.cpp index 161d6df419..b783cc0ab3 100644 --- a/Source/Core/DolphinQt/GameList/GameTracker.cpp +++ b/Source/Core/DolphinQt/GameList/GameTracker.cpp @@ -27,7 +27,7 @@ static const QStringList game_filters{ QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"), QStringLiteral("*.[wW][iI][aA]"), QStringLiteral("*.[rR][vV][zZ]"), QStringLiteral("*.[wW][aA][dD]"), QStringLiteral("*.[eE][lL][fF]"), - QStringLiteral("*.[dD][oO][lL]")}; + QStringLiteral("*.[dD][oO][lL]"), QStringLiteral("*.[jJ][sS][oO][nN]")}; GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent) { diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index b0bb587e7e..778fcce6ce 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -733,8 +733,10 @@ QStringList MainWindow::PromptFileNames() QStringList paths = DolphinFileDialog::getOpenFileNames( this, tr("Select a File"), settings.value(QStringLiteral("mainwindow/lastdir"), QString{}).toString(), - tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad " - "*.dff *.m3u);;All Files (*)")); + QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad " + "*.dff *.m3u *.json);;%2 (*)") + .arg(tr("All GC/Wii files")) + .arg(tr("All Files"))); if (!paths.isEmpty()) { diff --git a/Source/Core/DolphinQt/Settings/PathPane.cpp b/Source/Core/DolphinQt/Settings/PathPane.cpp index 41fe993f36..3f1bf93e28 100644 --- a/Source/Core/DolphinQt/Settings/PathPane.cpp +++ b/Source/Core/DolphinQt/Settings/PathPane.cpp @@ -44,8 +44,10 @@ void PathPane::BrowseDefaultGame() { QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName( this, tr("Select a Game"), Settings::Instance().GetDefaultGame(), - tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs " - "*.ciso *.gcz *.wia *.rvz *.wad *.m3u);;All Files (*)"))); + QStringLiteral("%1 (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wia *.rvz *.wad " + "*.m3u *.json);;%2 (*)") + .arg(tr("All GC/Wii files")) + .arg(tr("All Files")))); if (!file.isEmpty()) Settings::Instance().SetDefaultGame(file); diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index 367ea65642..3417051a54 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -43,6 +43,7 @@ #include "DiscIO/Blob.h" #include "DiscIO/DiscExtractor.h" #include "DiscIO/Enums.h" +#include "DiscIO/GameModDescriptor.h" #include "DiscIO/Volume.h" #include "DiscIO/WiiSaveBanner.h" @@ -163,6 +164,32 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path)) m_platform = DiscIO::Platform::ELFOrDOL; m_blob_type = DiscIO::BlobType::DIRECTORY; } + + if (!IsValid() && GetExtension() == ".json") + { + auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path); + if (descriptor) + { + GameFile proxy(descriptor->base_file); + if (proxy.IsValid()) + { + m_valid = true; + m_file_size = File::GetSize(m_file_path); + m_long_names.emplace(DiscIO::Language::English, std::move(descriptor->display_name)); + m_internal_name = proxy.GetInternalName(); + m_game_id = proxy.GetGameID(); + m_gametdb_id = proxy.GetGameTDBID(); + m_title_id = proxy.GetTitleID(); + m_maker_id = proxy.GetMakerID(); + m_region = proxy.GetRegion(); + m_country = proxy.GetCountry(); + m_platform = proxy.GetPlatform(); + m_revision = proxy.GetRevision(); + m_disc_number = proxy.GetDiscNumber(); + m_blob_type = DiscIO::BlobType::MOD_DESCRIPTOR; + } + } + } } GameFile::~GameFile() = default; @@ -470,6 +497,18 @@ bool GameFile::ReadPNGBanner(const std::string& path) return true; } +bool GameFile::TryLoadGameModDescriptorBanner() +{ + if (m_blob_type != DiscIO::BlobType::MOD_DESCRIPTOR) + return false; + + auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path); + if (!descriptor) + return false; + + return ReadPNGBanner(descriptor->banner); +} + bool GameFile::CustomBannerChanged() { std::string path, name; @@ -482,8 +521,12 @@ bool GameFile::CustomBannerChanged() // Homebrew Channel icon naming. Typical for DOLs and ELFs, but we also support it for volumes. if (!ReadPNGBanner(path + "icon.png")) { - // If no custom icon is found, go back to the non-custom one. - m_pending.custom_banner = {}; + // If it's a game mod descriptor file, it may specify its own custom banner. + if (!TryLoadGameModDescriptorBanner()) + { + // If no custom icon is found, go back to the non-custom one. + m_pending.custom_banner = {}; + } } } @@ -499,6 +542,8 @@ const std::string& GameFile::GetName(const Core::TitleDatabase& title_database) { if (!m_custom_name.empty()) return m_custom_name; + if (IsModDescriptor()) + return GetName(Variant::LongAndPossiblyCustom); const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage()); return database_name.empty() ? GetName(Variant::LongAndPossiblyCustom) : database_name; @@ -652,6 +697,7 @@ bool GameFile::ShouldShowFileFormatDetails() const case DiscIO::BlobType::PLAIN: break; case DiscIO::BlobType::DRIVE: + case DiscIO::BlobType::MOD_DESCRIPTOR: return false; default: return true; @@ -699,6 +745,11 @@ bool GameFile::ShouldAllowConversion() const return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate; } +bool GameFile::IsModDescriptor() const +{ + return m_blob_type == DiscIO::BlobType::MOD_DESCRIPTOR; +} + const GameBanner& GameFile::GetBannerImage() const { return m_custom_banner.empty() ? m_volume_banner : m_custom_banner; diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h index fc981cd226..eb1151a28a 100644 --- a/Source/Core/UICommon/GameFile.h +++ b/Source/Core/UICommon/GameFile.h @@ -107,6 +107,7 @@ public: bool IsVolumeSizeAccurate() const { return m_volume_size_is_accurate; } bool IsDatelDisc() const { return m_is_datel_disc; } bool IsNKit() const { return m_is_nkit; } + bool IsModDescriptor() const; const GameBanner& GetBannerImage() const; const GameCover& GetCoverImage() const; void DoState(PointerWrap& p); @@ -132,6 +133,7 @@ private: bool IsElfOrDol() const; bool ReadXMLMetadata(const std::string& path); bool ReadPNGBanner(const std::string& path); + bool TryLoadGameModDescriptorBanner(); // IMPORTANT: Nearly all data members must be save/restored in DoState. // If anything is changed, make sure DoState handles it properly and diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index a1b55b3614..1040c24b2f 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -27,13 +27,14 @@ namespace UICommon { -static constexpr u32 CACHE_REVISION = 20; // Last changed in PR 9461 +static constexpr u32 CACHE_REVISION = 21; // Last changed in PR 10187 std::vector FindAllGamePaths(const std::vector& directories_to_scan, bool recursive_scan) { - static const std::vector search_extensions = { - ".gcm", ".tgc", ".iso", ".ciso", ".gcz", ".wbfs", ".wia", ".rvz", ".wad", ".dol", ".elf"}; + static const std::vector search_extensions = {".gcm", ".tgc", ".iso", ".ciso", + ".gcz", ".wbfs", ".wia", ".rvz", + ".wad", ".dol", ".elf", ".json"}; // TODO: We could process paths iteratively as they are found return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);