From b50861ea44e237e69b1c931b4ceb8c9b6dc8db4e Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 9 Dec 2021 00:01:10 +0100 Subject: [PATCH 1/8] Core: Require game mod descriptor json to self-identify as one. --- Source/Core/DiscIO/GameModDescriptor.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Core/DiscIO/GameModDescriptor.cpp b/Source/Core/DiscIO/GameModDescriptor.cpp index 587faafdaf..f1a50ac649 100644 --- a/Source/Core/DiscIO/GameModDescriptor.cpp +++ b/Source/Core/DiscIO/GameModDescriptor.cpp @@ -115,10 +115,15 @@ std::optional ParseGameModDescriptorString(std::string_view j return std::nullopt; GameModDescriptor descriptor; + bool is_game_mod_descriptor = false; bool is_valid_version = false; for (const auto& [key, value] : json_root.get()) { - if (key == "version" && value.is()) + if (key == "type" && value.is()) + { + is_game_mod_descriptor = value.get() == "dolphin-game-mod-descriptor"; + } + else if (key == "version" && value.is()) { is_valid_version = value.get() == 1.0; } @@ -140,7 +145,7 @@ std::optional ParseGameModDescriptorString(std::string_view j ParseRiivolutionObject(json_directory, value.get()); } } - if (!is_valid_version) + if (!is_game_mod_descriptor || !is_valid_version) return std::nullopt; return descriptor; } From aa0595589aa10d5c6958b61814628d8c12f3f087 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 8 Dec 2021 23:49:30 +0100 Subject: [PATCH 2/8] docs: Add JSON Schema for game mod descriptors. --- docs/game-mod-descriptor.json | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/game-mod-descriptor.json diff --git a/docs/game-mod-descriptor.json b/docs/game-mod-descriptor.json new file mode 100644 index 0000000000..a78486f6c1 --- /dev/null +++ b/docs/game-mod-descriptor.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://raw.githubusercontent.com/dolphin-emu/dolphin/master/docs/game-mod-descriptor.json", + "title": "Dolphin Game Mod Descriptor", + "type": "object", + "required": ["type", "version", "base-file"], + "properties": { + "type": { + "type": "string", + "pattern": "^dolphin-game-mod-descriptor$" + }, + "version": { + "type": "integer" + }, + "base-file": { + "type": "string" + }, + "display-name": { + "type": "string" + }, + "banner": { + "type": "string" + }, + "riivolution": { + "type": "object", + "required": ["patches"], + "properties": { + "patches": { + "type": "array", + "items": { + "type": "object", + "required": ["xml", "root", "options"], + "properties": { + "xml": { + "type": "string" + }, + "root": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "required": ["choice"], + "properties": { + "section-name": { + "type": "string" + }, + "option-id": { + "type": "string" + }, + "option-name": { + "type": "string" + }, + "choice": { + "type": "integer" + } + } + } + } + } + } + } + } + } + } +} From da161faff46d9de45b4e80ebf498fc11f85bb493 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 26 Oct 2021 02:01:16 +0200 Subject: [PATCH 3/8] GameList: Show game mod descriptor .json files in game list. --- .../dolphinemu/utils/FileBrowserHelper.java | 2 +- Source/Core/DiscIO/Blob.cpp | 2 + Source/Core/DiscIO/Blob.h | 1 + Source/Core/DolphinQt/GameList/GameList.cpp | 14 +++-- .../Core/DolphinQt/GameList/GameTracker.cpp | 2 +- Source/Core/DolphinQt/MainWindow.cpp | 6 +- Source/Core/DolphinQt/Settings/PathPane.cpp | 6 +- Source/Core/UICommon/GameFile.cpp | 55 ++++++++++++++++++- Source/Core/UICommon/GameFile.h | 2 + Source/Core/UICommon/GameFileCache.cpp | 7 ++- 10 files changed, 80 insertions(+), 17 deletions(-) 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); From a2a39cfcfb3874e1a15d0a4c84e7d17abdf66018 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 26 Oct 2021 02:55:29 +0200 Subject: [PATCH 4/8] Core: Add ability to serialize a GameModDescriptor to json. --- Source/Core/DiscIO/GameModDescriptor.cpp | 72 ++++++++++++++++++++++++ Source/Core/DiscIO/GameModDescriptor.h | 3 + 2 files changed, 75 insertions(+) diff --git a/Source/Core/DiscIO/GameModDescriptor.cpp b/Source/Core/DiscIO/GameModDescriptor.cpp index f1a50ac649..016df59d3d 100644 --- a/Source/Core/DiscIO/GameModDescriptor.cpp +++ b/Source/Core/DiscIO/GameModDescriptor.cpp @@ -149,4 +149,76 @@ std::optional ParseGameModDescriptorString(std::string_view j return std::nullopt; return descriptor; } + +static picojson::object +WriteGameModDescriptorRiivolution(const GameModDescriptorRiivolution& riivolution) +{ + picojson::array json_patches; + for (const auto& patch : riivolution.patches) + { + picojson::object json_patch; + if (!patch.xml.empty()) + json_patch["xml"] = picojson::value(patch.xml); + if (!patch.root.empty()) + json_patch["root"] = picojson::value(patch.root); + if (!patch.options.empty()) + { + picojson::array json_options; + for (const auto& option : patch.options) + { + picojson::object json_option; + if (!option.section_name.empty()) + json_option["section-name"] = picojson::value(option.section_name); + if (!option.option_id.empty()) + json_option["option-id"] = picojson::value(option.option_id); + if (!option.option_name.empty()) + json_option["option-name"] = picojson::value(option.option_name); + json_option["choice"] = picojson::value(static_cast(option.choice)); + json_options.emplace_back(std::move(json_option)); + } + json_patch["options"] = picojson::value(std::move(json_options)); + } + json_patches.emplace_back(std::move(json_patch)); + } + + picojson::object json_riivolution; + json_riivolution["patches"] = picojson::value(std::move(json_patches)); + return json_riivolution; +} + +std::string WriteGameModDescriptorString(const GameModDescriptor& descriptor, bool pretty) +{ + picojson::object json_root; + json_root["type"] = picojson::value("dolphin-game-mod-descriptor"); + json_root["version"] = picojson::value(1.0); + if (!descriptor.base_file.empty()) + json_root["base-file"] = picojson::value(descriptor.base_file); + if (!descriptor.display_name.empty()) + json_root["display-name"] = picojson::value(descriptor.display_name); + if (!descriptor.banner.empty()) + json_root["banner"] = picojson::value(descriptor.banner); + if (descriptor.riivolution) + { + json_root["riivolution"] = + picojson::value(WriteGameModDescriptorRiivolution(*descriptor.riivolution)); + } + return picojson::value(json_root).serialize(pretty); +} + +bool WriteGameModDescriptorFile(const std::string& filename, const GameModDescriptor& descriptor, + bool pretty) +{ + auto json = WriteGameModDescriptorString(descriptor, pretty); + if (json.empty()) + return false; + + ::File::IOFile f(filename, "wb"); + if (!f) + return false; + + if (!f.WriteString(json)) + return false; + + return true; +} } // namespace DiscIO diff --git a/Source/Core/DiscIO/GameModDescriptor.h b/Source/Core/DiscIO/GameModDescriptor.h index 09a03ed2df..dc9c5d06f9 100644 --- a/Source/Core/DiscIO/GameModDescriptor.h +++ b/Source/Core/DiscIO/GameModDescriptor.h @@ -43,4 +43,7 @@ struct GameModDescriptor std::optional ParseGameModDescriptorFile(const std::string& filename); std::optional ParseGameModDescriptorString(std::string_view json, std::string_view json_path); +std::string WriteGameModDescriptorString(const GameModDescriptor& descriptor, bool pretty); +bool WriteGameModDescriptorFile(const std::string& filename, const GameModDescriptor& descriptor, + bool pretty); } // namespace DiscIO From 005e850ad6064f3ef6344678e4019ca940d56b72 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 26 Oct 2021 03:18:06 +0200 Subject: [PATCH 5/8] DolphinQt: Add a 'Save as Preset' button to RiivolutionBootWidget. --- Source/Core/DolphinQt/MainWindow.cpp | 2 +- .../Core/DolphinQt/RiivolutionBootWidget.cpp | 66 +++++++++++++++++-- Source/Core/DolphinQt/RiivolutionBootWidget.h | 8 ++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 778fcce6ce..be391bea11 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -1847,7 +1847,7 @@ void MainWindow::ShowRiivolutionBootWidget(const UICommon::GameFile& game) auto& disc = std::get(boot_params->parameters); RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(), - disc.volume->GetDiscNumber(), this); + disc.volume->GetDiscNumber(), game.GetFilePath(), this); w.exec(); if (!w.ShouldBoot()) return; diff --git a/Source/Core/DolphinQt/RiivolutionBootWidget.cpp b/Source/Core/DolphinQt/RiivolutionBootWidget.cpp index ecb4ac078a..c70df8e770 100644 --- a/Source/Core/DolphinQt/RiivolutionBootWidget.cpp +++ b/Source/Core/DolphinQt/RiivolutionBootWidget.cpp @@ -23,6 +23,7 @@ #include "Common/FileSearch.h" #include "Common/FileUtil.h" +#include "DiscIO/GameModDescriptor.h" #include "DiscIO/RiivolutionParser.h" #include "DiscIO/RiivolutionPatcher.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" @@ -38,8 +39,10 @@ struct GuiRiivolutionPatchIndex Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex); RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional revision, - std::optional disc, QWidget* parent) - : QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc) + std::optional disc, std::string base_game_path, + QWidget* parent) + : QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc), + m_base_game_path(std::move(base_game_path)) { setWindowTitle(tr("Start with Riivolution Patches")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -57,6 +60,7 @@ void RiivolutionBootWidget::CreateWidgets() auto* open_xml_button = new QPushButton(tr("Open Riivolution XML...")); auto* boot_game_button = new QPushButton(tr("Start")); boot_game_button->setDefault(true); + auto* save_preset_button = new QPushButton(tr("Save as Preset...")); auto* group_box = new QGroupBox(); auto* scroll_area = new QScrollArea(); @@ -71,6 +75,7 @@ void RiivolutionBootWidget::CreateWidgets() auto* button_layout = new QHBoxLayout(); button_layout->addStretch(); button_layout->addWidget(open_xml_button, 0, Qt::AlignRight); + button_layout->addWidget(save_preset_button, 0, Qt::AlignRight); button_layout->addWidget(boot_game_button, 0, Qt::AlignRight); auto* layout = new QVBoxLayout(); @@ -80,6 +85,7 @@ void RiivolutionBootWidget::CreateWidgets() connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML); connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame); + connect(save_preset_button, &QPushButton::clicked, this, &RiivolutionBootWidget::SaveAsPreset); } void RiivolutionBootWidget::LoadMatchingXMLs() @@ -144,13 +150,14 @@ void RiivolutionBootWidget::OpenXML() } } -void RiivolutionBootWidget::MakeGUIForParsedFile(const std::string& path, std::string root, +void RiivolutionBootWidget::MakeGUIForParsedFile(std::string path, std::string root, DiscIO::Riivolution::Disc input_disc) { const size_t disc_index = m_discs.size(); - const auto& disc = m_discs.emplace_back(DiscWithRoot{std::move(input_disc), std::move(root)}); + const auto& disc = + m_discs.emplace_back(DiscWithRoot{std::move(input_disc), std::move(root), std::move(path)}); - auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(path)).fileName()); + auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(disc.path)).fileName()); auto* disc_layout = new QVBoxLayout(); disc_box->setLayout(disc_layout); @@ -279,3 +286,52 @@ void RiivolutionBootWidget::BootGame() m_should_boot = true; close(); } + +void RiivolutionBootWidget::SaveAsPreset() +{ + DiscIO::GameModDescriptor descriptor; + descriptor.base_file = m_base_game_path; + + DiscIO::GameModDescriptorRiivolution riivolution_descriptor; + for (const auto& disc : m_discs) + { + // filter out XMLs that don't actually contribute to the preset + auto patches = disc.disc.GeneratePatches(m_game_id); + if (patches.empty()) + continue; + + auto& descriptor_patch = riivolution_descriptor.patches.emplace_back(); + descriptor_patch.xml = disc.path; + descriptor_patch.root = disc.root; + for (const auto& section : disc.disc.m_sections) + { + for (const auto& option : section.m_options) + { + auto& descriptor_option = descriptor_patch.options.emplace_back(); + descriptor_option.section_name = section.m_name; + if (!option.m_id.empty()) + descriptor_option.option_id = option.m_id; + else + descriptor_option.option_name = option.m_name; + descriptor_option.choice = option.m_selected_choice; + } + } + } + + if (!riivolution_descriptor.patches.empty()) + descriptor.riivolution = std::move(riivolution_descriptor); + + QDir dir = QFileInfo(QString::fromStdString(m_base_game_path)).dir(); + QString target_path = QFileDialog::getSaveFileName(this, tr("Save Preset"), dir.absolutePath(), + QStringLiteral("%1 (*.json);;%2 (*)") + .arg(tr("Dolphin Game Mod Preset")) + .arg(tr("All Files"))); + if (target_path.isEmpty()) + return; + + descriptor.display_name = QFileInfo(target_path).fileName().toStdString(); + auto dot = descriptor.display_name.rfind('.'); + if (dot != std::string::npos) + descriptor.display_name = descriptor.display_name.substr(0, dot); + DiscIO::WriteGameModDescriptorFile(target_path.toStdString(), descriptor, true); +} diff --git a/Source/Core/DolphinQt/RiivolutionBootWidget.h b/Source/Core/DolphinQt/RiivolutionBootWidget.h index 5200ca2151..1318241a11 100644 --- a/Source/Core/DolphinQt/RiivolutionBootWidget.h +++ b/Source/Core/DolphinQt/RiivolutionBootWidget.h @@ -19,7 +19,8 @@ class RiivolutionBootWidget : public QDialog Q_OBJECT public: explicit RiivolutionBootWidget(std::string game_id, std::optional revision, - std::optional disc, QWidget* parent = nullptr); + std::optional disc, std::string base_game_path, + QWidget* parent = nullptr); ~RiivolutionBootWidget(); bool ShouldBoot() const { return m_should_boot; } @@ -30,21 +31,24 @@ private: void LoadMatchingXMLs(); void OpenXML(); - void MakeGUIForParsedFile(const std::string& path, std::string root, + void MakeGUIForParsedFile(std::string path, std::string root, DiscIO::Riivolution::Disc input_disc); std::optional LoadConfigXML(const std::string& root_directory); void SaveConfigXMLs(); void BootGame(); + void SaveAsPreset(); std::string m_game_id; std::optional m_revision; std::optional m_disc_number; + std::string m_base_game_path; bool m_should_boot = false; struct DiscWithRoot { DiscIO::Riivolution::Disc disc; std::string root; + std::string path; }; std::vector m_discs; std::vector m_patches; From 418bb0b0878063876579e1ed142dcad64a243c0a Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sat, 13 Nov 2021 21:15:45 +0100 Subject: [PATCH 6/8] GameFile: Calculate a sensible sync hash for mod descriptors. --- Source/Core/UICommon/GameFile.cpp | 71 ++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index 3417051a54..c575d4018b 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -22,6 +22,7 @@ #include #include +#include "Common/BitUtils.h" #include "Common/ChunkFile.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" @@ -624,15 +625,75 @@ std::string GameFile::GetNetPlayName(const Core::TitleDatabase& title_database) return name + " (" + ss.str() + ")"; } +static std::array GetHash(u32 value) +{ + auto data = Common::BitCastToArray(value); + std::array hash; + mbedtls_sha1_ret(reinterpret_cast(data.data()), data.size(), hash.data()); + return hash; +} + +static std::array GetHash(std::string_view str) +{ + std::array hash; + mbedtls_sha1_ret(reinterpret_cast(str.data()), str.size(), hash.data()); + return hash; +} + +static std::optional> GetFileHash(const std::string& path) +{ + std::string buffer; + if (!File::ReadFileToString(path, buffer)) + return std::nullopt; + return GetHash(buffer); +} + +static std::optional> MixHash(const std::optional>& lhs, + const std::optional>& rhs) +{ + if (!lhs && !rhs) + return std::nullopt; + if (!lhs || !rhs) + return !rhs ? lhs : rhs; + std::array result; + for (size_t i = 0; i < result.size(); ++i) + result[i] = (*lhs)[i] ^ (*rhs)[(i + 1) % result.size()]; + return result; +} + std::array GameFile::GetSyncHash() const { - std::array hash{}; + std::optional> hash; if (m_platform == DiscIO::Platform::ELFOrDOL) { - std::string buffer; - if (File::ReadFileToString(m_file_path, buffer)) - mbedtls_sha1_ret(reinterpret_cast(buffer.data()), buffer.size(), hash.data()); + hash = GetFileHash(m_file_path); + } + else if (m_blob_type == DiscIO::BlobType::MOD_DESCRIPTOR) + { + auto descriptor = DiscIO::ParseGameModDescriptorFile(m_file_path); + if (descriptor) + { + GameFile proxy(descriptor->base_file); + if (proxy.IsValid()) + hash = proxy.GetSyncHash(); + + // add patches to hash if they're enabled + if (descriptor->riivolution) + { + for (const auto& patch : descriptor->riivolution->patches) + { + hash = MixHash(hash, GetFileHash(patch.xml)); + for (const auto& option : patch.options) + { + hash = MixHash(hash, GetHash(option.section_name)); + hash = MixHash(hash, GetHash(option.option_id)); + hash = MixHash(hash, GetHash(option.option_name)); + hash = MixHash(hash, GetHash(option.choice)); + } + } + } + } } else { @@ -640,7 +701,7 @@ std::array GameFile::GetSyncHash() const hash = volume->GetSyncHash(); } - return hash; + return hash.value_or(std::array{}); } NetPlay::SyncIdentifier GameFile::GetSyncIdentifier() const From 387d14835716389b5163b6a43ccda6f46da4b4df Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 14 Nov 2021 02:21:58 +0100 Subject: [PATCH 7/8] NetPlay: Add functions to sync folders. --- Source/Core/Core/NetPlayCommon.cpp | 71 ++++++++++++++++++++++++++++++ Source/Core/Core/NetPlayCommon.h | 2 + 2 files changed, 73 insertions(+) diff --git a/Source/Core/Core/NetPlayCommon.cpp b/Source/Core/Core/NetPlayCommon.cpp index 910c7308fd..a72e03236b 100644 --- a/Source/Core/Core/NetPlayCommon.cpp +++ b/Source/Core/Core/NetPlayCommon.cpp @@ -3,6 +3,7 @@ #include "Core/NetPlayCommon.h" +#include #include #include "Common/FileUtil.h" @@ -84,6 +85,35 @@ bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet) return true; } +static bool CompressFolderIntoPacketInternal(const File::FSTEntry& folder, sf::Packet& packet) +{ + const sf::Uint64 size = folder.children.size(); + packet << size; + for (const auto& child : folder.children) + { + const bool is_folder = child.isDirectory; + packet << child.virtualName; + packet << is_folder; + const bool success = is_folder ? CompressFolderIntoPacketInternal(child, packet) : + CompressFileIntoPacket(child.physicalName, packet); + if (!success) + return false; + } + return true; +} + +bool CompressFolderIntoPacket(const std::string& folder_path, sf::Packet& packet) +{ + if (!File::IsDirectory(folder_path)) + { + packet << false; + return true; + } + + packet << true; + return CompressFolderIntoPacketInternal(File::ScanDirectoryTree(folder_path, true), packet); +} + bool CompressBufferIntoPacket(const std::vector& in_buffer, sf::Packet& packet) { const sf::Uint64 size = in_buffer.size(); @@ -187,6 +217,47 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path) return true; } +static bool DecompressPacketIntoFolderInternal(sf::Packet& packet, const std::string& folder_path) +{ + if (!File::CreateFullPath(folder_path + "/")) + return false; + + sf::Uint64 size; + packet >> size; + for (size_t i = 0; i < size; ++i) + { + std::string name; + packet >> name; + + if (name.find('/') != std::string::npos) + return false; +#ifdef _WIN32 + if (name.find('\\') != std::string::npos) + return false; +#endif + if (std::all_of(name.begin(), name.end(), [](char c) { return c == '.'; })) + return false; + + bool is_folder; + packet >> is_folder; + std::string path = fmt::format("{}/{}", folder_path, name); + const bool success = is_folder ? DecompressPacketIntoFolderInternal(packet, path) : + DecompressPacketIntoFile(packet, path); + if (!success) + return false; + } + return true; +} + +bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_path) +{ + bool folder_existed; + packet >> folder_existed; + if (!folder_existed) + return true; + return DecompressPacketIntoFolderInternal(packet, folder_path); +} + std::optional> DecompressPacketIntoBuffer(sf::Packet& packet) { u64 size = Common::PacketReadU64(packet); diff --git a/Source/Core/Core/NetPlayCommon.h b/Source/Core/Core/NetPlayCommon.h index fc2a017047..9893131772 100644 --- a/Source/Core/Core/NetPlayCommon.h +++ b/Source/Core/Core/NetPlayCommon.h @@ -15,7 +15,9 @@ namespace NetPlay { bool CompressFileIntoPacket(const std::string& file_path, sf::Packet& packet); +bool CompressFolderIntoPacket(const std::string& folder_path, sf::Packet& packet); bool CompressBufferIntoPacket(const std::vector& in_buffer, sf::Packet& packet); bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path); +bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_path); std::optional> DecompressPacketIntoBuffer(sf::Packet& packet); } // namespace NetPlay From b928900f6e1097a189cd78491df00ed7890949d6 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 14 Nov 2021 02:22:59 +0100 Subject: [PATCH 8/8] Core/WiiRoot: Handle the combination of NetPlay and savegame redirects. --- Source/Core/Core/Boot/Boot.cpp | 9 ++- Source/Core/Core/Boot/Boot.h | 4 +- Source/Core/Core/NetPlayClient.cpp | 42 ++++++++-- Source/Core/Core/NetPlayClient.h | 6 +- Source/Core/Core/NetPlayServer.cpp | 27 ++++++- Source/Core/Core/WiiRoot.cpp | 80 ++++++++++++++----- .../Core/DolphinQt/NetPlay/NetPlayDialog.cpp | 4 +- Source/Core/DolphinQt/NetPlay/NetPlayDialog.h | 2 +- 8 files changed, 140 insertions(+), 34 deletions(-) diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index b34e9bbbe5..22f9b4f017 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -155,6 +155,11 @@ const std::vector& BootSessionData::GetWiiSyncTitles() const return m_wii_sync_titles; } +const std::string& BootSessionData::GetWiiSyncRedirectFolder() const +{ + return m_wii_sync_redirect_folder; +} + void BootSessionData::InvokeWiiSyncCleanup() const { if (m_wii_sync_cleanup) @@ -162,10 +167,12 @@ void BootSessionData::InvokeWiiSyncCleanup() const } void BootSessionData::SetWiiSyncData(std::unique_ptr fs, - std::vector titles, WiiSyncCleanupFunction cleanup) + std::vector titles, std::string redirect_folder, + WiiSyncCleanupFunction cleanup) { m_wii_sync_fs = std::move(fs); m_wii_sync_titles = std::move(titles); + m_wii_sync_redirect_folder = std::move(redirect_folder); m_wii_sync_cleanup = std::move(cleanup); } diff --git a/Source/Core/Core/Boot/Boot.h b/Source/Core/Core/Boot/Boot.h index e27d6a41df..236dddaeca 100644 --- a/Source/Core/Core/Boot/Boot.h +++ b/Source/Core/Core/Boot/Boot.h @@ -66,9 +66,10 @@ public: IOS::HLE::FS::FileSystem* GetWiiSyncFS() const; const std::vector& GetWiiSyncTitles() const; + const std::string& GetWiiSyncRedirectFolder() const; void InvokeWiiSyncCleanup() const; void SetWiiSyncData(std::unique_ptr fs, std::vector titles, - WiiSyncCleanupFunction cleanup); + std::string redirect_folder, WiiSyncCleanupFunction cleanup); private: std::optional m_savestate_path; @@ -76,6 +77,7 @@ private: std::unique_ptr m_wii_sync_fs; std::vector m_wii_sync_titles; + std::string m_wii_sync_redirect_folder; WiiSyncCleanupFunction m_wii_sync_cleanup; }; diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index 66fc47e253..45782dfe21 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -1077,6 +1077,7 @@ void NetPlayClient::OnSyncSaveDataGCI(sf::Packet& packet) void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet) { const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP; + std::string redirect_path = File::GetUserPath(D_USER_IDX) + "Redirect" GC_MEMCARD_NETPLAY DIR_SEP; if (File::Exists(path) && !File::DeleteDirRecursively(path)) { @@ -1084,6 +1085,12 @@ void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet) SyncSaveDataResponse(false); return; } + if (File::Exists(redirect_path) && !File::DeleteDirRecursively(redirect_path)) + { + PanicAlertFmtT("Failed to reset NetPlay redirect folder. Verify your write permissions."); + SyncSaveDataResponse(false); + return; + } auto temp_fs = std::make_unique(path); std::vector titles; @@ -1190,7 +1197,19 @@ void NetPlayClient::OnSyncSaveDataWii(sf::Packet& packet) } } - SetWiiSyncData(std::move(temp_fs), std::move(titles)); + bool has_redirected_save; + packet >> has_redirected_save; + if (has_redirected_save) + { + if (!DecompressPacketIntoFolder(packet, redirect_path)) + { + PanicAlertFmtT("Failed to write redirected save."); + SyncSaveDataResponse(false); + return; + } + } + + SetWiiSyncData(std::move(temp_fs), std::move(titles), std::move(redirect_path)); SyncSaveDataResponse(true); } @@ -1721,12 +1740,18 @@ bool NetPlayClient::StartGame(const std::string& path) // boot game auto boot_session_data = std::make_unique(); - boot_session_data->SetWiiSyncData(std::move(m_wii_sync_fs), std::move(m_wii_sync_titles), [] { - // on emulation end clean up the Wii save sync directory -- see OnSyncSaveDataWii() - const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP; - if (File::Exists(path)) - File::DeleteDirRecursively(path); - }); + boot_session_data->SetWiiSyncData( + std::move(m_wii_sync_fs), std::move(m_wii_sync_titles), std::move(m_wii_sync_redirect_folder), + [] { + // on emulation end clean up the Wii save sync directory -- see OnSyncSaveDataWii() + const std::string path = File::GetUserPath(D_USER_IDX) + "Wii" GC_MEMCARD_NETPLAY DIR_SEP; + if (File::Exists(path)) + File::DeleteDirRecursively(path); + const std::string redirect_path = + File::GetUserPath(D_USER_IDX) + "Redirect" GC_MEMCARD_NETPLAY DIR_SEP; + if (File::Exists(redirect_path)) + File::DeleteDirRecursively(redirect_path); + }); m_dialog->BootGame(path, std::move(boot_session_data)); UpdateDevices(); @@ -2501,10 +2526,11 @@ void NetPlayClient::AdjustPadBufferSize(const unsigned int size) } void NetPlayClient::SetWiiSyncData(std::unique_ptr fs, - std::vector titles) + std::vector titles, std::string redirect_folder) { m_wii_sync_fs = std::move(fs); m_wii_sync_titles = std::move(titles); + m_wii_sync_redirect_folder = std::move(redirect_folder); } SyncIdentifier NetPlayClient::GetSDCardIdentifier() diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index 24cf494634..684d1a2616 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -86,7 +86,7 @@ public: virtual void HideChunkedProgressDialog() = 0; virtual void SetChunkedProgress(int pid, u64 progress) = 0; - virtual void SetHostWiiSyncTitles(std::vector titles) = 0; + virtual void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) = 0; }; class Player @@ -157,7 +157,8 @@ public: void AdjustPadBufferSize(unsigned int size); - void SetWiiSyncData(std::unique_ptr fs, std::vector titles); + void SetWiiSyncData(std::unique_ptr fs, std::vector titles, + std::string redirect_folder); static SyncIdentifier GetSDCardIdentifier(); @@ -328,6 +329,7 @@ private: std::unique_ptr m_wii_sync_fs; std::vector m_wii_sync_titles; + std::string m_wii_sync_redirect_folder; }; void NetPlay_Enable(NetPlayClient* const np); diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index c0548a3384..e25f881be8 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -31,6 +31,7 @@ #include "Common/Version.h" #include "Core/ActionReplay.h" +#include "Core/Boot/Boot.h" #include "Core/Config/GraphicsSettings.h" #include "Core/Config/MainSettings.h" #include "Core/Config/NetplaySettings.h" @@ -60,6 +61,7 @@ #include "Core/SyncIdentifier.h" #include "DiscIO/Enums.h" +#include "DiscIO/RiivolutionPatcher.h" #include "InputCommon/ControllerEmu/ControlGroup/Attachments.h" #include "InputCommon/GCPadStatus.h" @@ -1616,6 +1618,17 @@ bool NetPlayServer::SyncSaveData() save_count++; } + std::optional redirected_save; + if (wii_save && game->GetBlobType() == DiscIO::BlobType::MOD_DESCRIPTOR) + { + auto boot_params = BootParameters::GenerateFromFile(game->GetFilePath()); + if (boot_params) + { + redirected_save = + DiscIO::Riivolution::ExtractSavegameRedirect(boot_params->riivolution_patches); + } + } + for (const auto& config : m_gba_config) { if (config.enabled && config.has_rom) @@ -1818,8 +1831,20 @@ bool NetPlayServer::SyncSaveData() } } + if (redirected_save) + { + pac << true; + if (!CompressFolderIntoPacket(redirected_save->m_target_path, pac)) + return false; + } + else + { + pac << false; // no redirected save + } + // Set titles for host-side loading in WiiRoot - m_dialog->SetHostWiiSyncTitles(std::move(titles)); + m_dialog->SetHostWiiSyncData(std::move(titles), + redirected_save ? redirected_save->m_target_path : ""); SendChunkedToClients(std::move(pac), 1, "Wii Save Synchronization"); } diff --git a/Source/Core/Core/WiiRoot.cpp b/Source/Core/Core/WiiRoot.cpp index 2af5901dfb..149eb50627 100644 --- a/Source/Core/Core/WiiRoot.cpp +++ b/Source/Core/Core/WiiRoot.cpp @@ -35,9 +35,19 @@ namespace Core namespace FS = IOS::HLE::FS; static std::string s_temp_wii_root; +static std::string s_temp_redirect_root; static bool s_wii_root_initialized = false; static std::vector s_nand_redirects; +// When Temp NAND + Redirects are both active, we need to keep track of where each redirect path +// should be copied back to after a successful session finish. +struct TempRedirectPath +{ + std::string real_path; + std::string temp_path; +}; +static std::vector s_temp_nand_redirects; + const std::vector& GetActiveNandRedirects() { return s_nand_redirects; @@ -175,6 +185,28 @@ static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs, WARN_LOG_FMT(CORE, "Failed to copy Mii database to the NAND"); } } + + const auto& netplay_redirect_folder = boot_session_data.GetWiiSyncRedirectFolder(); + if (!netplay_redirect_folder.empty()) + File::CopyDir(netplay_redirect_folder, s_temp_redirect_root + "/"); + } +} + +static void MoveToBackupIfExists(const std::string& path) +{ + if (File::Exists(path)) + { + const std::string backup_path = path.substr(0, path.size() - 1) + ".backup" DIR_SEP; + WARN_LOG_FMT(IOS_FS, "Temporary directory at {} exists, moving to backup...", path); + + // If backup exists, delete it as we don't want a mess + if (File::Exists(backup_path)) + { + WARN_LOG_FMT(IOS_FS, "Temporary backup directory at {} exists, deleting...", backup_path); + File::DeleteDirRecursively(backup_path); + } + + File::CopyDir(path, backup_path, true); } } @@ -185,24 +217,13 @@ void InitializeWiiRoot(bool use_temporary) if (use_temporary) { s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP; + s_temp_redirect_root = File::GetUserPath(D_USER_IDX) + "RedirectSession" DIR_SEP; WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for minimal Wii FS", s_temp_wii_root); + WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for redirected saves", s_temp_redirect_root); // If directory exists, make a backup - if (File::Exists(s_temp_wii_root)) - { - const std::string backup_path = - s_temp_wii_root.substr(0, s_temp_wii_root.size() - 1) + ".backup" DIR_SEP; - WARN_LOG_FMT(IOS_FS, "Temporary Wii FS directory exists, moving to backup..."); - - // If backup exists, delete it as we don't want a mess - if (File::Exists(backup_path)) - { - WARN_LOG_FMT(IOS_FS, "Temporary Wii FS backup directory exists, deleting..."); - File::DeleteDirRecursively(backup_path); - } - - File::CopyDir(s_temp_wii_root, backup_path, true); - } + MoveToBackupIfExists(s_temp_wii_root); + MoveToBackupIfExists(s_temp_redirect_root); File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root); } @@ -221,6 +242,9 @@ void ShutdownWiiRoot() { File::DeleteDirRecursively(s_temp_wii_root); s_temp_wii_root.clear(); + File::DeleteDirRecursively(s_temp_redirect_root); + s_temp_redirect_root.clear(); + s_temp_nand_redirects.clear(); } s_nand_redirects.clear(); @@ -312,7 +336,8 @@ void InitializeWiiFileSystemContents( if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, "")) WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND"); - if (WiiRootIsTemporary()) + const bool is_temp_nand = WiiRootIsTemporary(); + if (is_temp_nand) { // Generate a SYSCONF with default settings for the temporary Wii NAND. SysConf sysconf{fs}; @@ -320,16 +345,26 @@ void InitializeWiiFileSystemContents( InitializeDeterministicWiiSaves(fs.get(), boot_session_data); } - else if (save_redirect) + + if (save_redirect) { const u64 title_id = SConfig::GetInstance().GetTitleID(); std::string source_path = Common::GetTitleDataPath(title_id); + + if (is_temp_nand) + { + // remember the actual path for copying back on shutdown and redirect to a temp folder instead + s_temp_nand_redirects.emplace_back( + TempRedirectPath{save_redirect->m_target_path, s_temp_redirect_root}); + save_redirect->m_target_path = s_temp_redirect_root; + } + if (!File::IsDirectory(save_redirect->m_target_path)) { File::CreateFullPath(save_redirect->m_target_path + "/"); if (save_redirect->m_clone) { - File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_CONFIGURED_ROOT), + File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_SESSION_ROOT), save_redirect->m_target_path); } } @@ -347,7 +382,16 @@ void CleanUpWiiFileSystemContents(const BootSessionData& boot_session_data) return; } + // copy back the temp nand redirected files to where they should normally be redirected to + for (const auto& redirect : s_temp_nand_redirects) + File::CopyDir(redirect.temp_path, redirect.real_path + "/", true); + IOS::HLE::EmulationKernel* ios = IOS::HLE::GetIOS(); + + // clear the redirects in the session FS, otherwise the back-copy might grab redirected files + s_nand_redirects.clear(); + ios->GetFS()->SetNandRedirects({}); + const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured); // Copy back Mii data diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index bcf5704a29..674fad4e5e 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -1179,9 +1179,9 @@ void NetPlayDialog::SetChunkedProgress(const int pid, const u64 progress) }); } -void NetPlayDialog::SetHostWiiSyncTitles(std::vector titles) +void NetPlayDialog::SetHostWiiSyncData(std::vector titles, std::string redirect_folder) { auto client = Settings::Instance().GetNetPlayClient(); if (client) - client->SetWiiSyncData(nullptr, std::move(titles)); + client->SetWiiSyncData(nullptr, std::move(titles), std::move(redirect_folder)); } diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h index fd14cea4e7..03111925bd 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h @@ -95,7 +95,7 @@ public: void HideChunkedProgressDialog() override; void SetChunkedProgress(int pid, u64 progress) override; - void SetHostWiiSyncTitles(std::vector titles) override; + void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) override; signals: void Stop();