diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 55ed606517..1482b9656c 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -47,6 +47,7 @@ #define SCREENSHOTS_DIR "ScreenShots" #define LOAD_DIR "Load" #define HIRES_TEXTURES_DIR "Textures" +#define RIIVOLUTION_DIR "Riivolution" #define DUMP_DIR "Dump" #define DUMP_TEXTURES_DIR "Textures" #define DUMP_FRAMES_DIR "Frames" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 1bc92c8d70..07d0d55c55 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -943,6 +943,7 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_SCREENSHOTS_IDX] = s_user_paths[D_USER_IDX] + SCREENSHOTS_DIR DIR_SEP; s_user_paths[D_LOAD_IDX] = s_user_paths[D_USER_IDX] + LOAD_DIR DIR_SEP; s_user_paths[D_HIRESTEXTURES_IDX] = s_user_paths[D_LOAD_IDX] + HIRES_TEXTURES_DIR DIR_SEP; + s_user_paths[D_RIIVOLUTION_IDX] = s_user_paths[D_LOAD_IDX] + RIIVOLUTION_DIR DIR_SEP; s_user_paths[D_DUMP_IDX] = s_user_paths[D_USER_IDX] + DUMP_DIR DIR_SEP; s_user_paths[D_DUMPFRAMES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_FRAMES_DIR DIR_SEP; s_user_paths[D_DUMPOBJECTS_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_OBJECTS_DIR DIR_SEP; @@ -1035,6 +1036,7 @@ static void RebuildUserDirectories(unsigned int dir_index) case D_LOAD_IDX: s_user_paths[D_HIRESTEXTURES_IDX] = s_user_paths[D_LOAD_IDX] + HIRES_TEXTURES_DIR DIR_SEP; + s_user_paths[D_RIIVOLUTION_IDX] = s_user_paths[D_LOAD_IDX] + RIIVOLUTION_DIR DIR_SEP; s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP; break; } diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 46bcdb150f..415c3fd102 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -41,6 +41,7 @@ enum D_STATESAVES_IDX, D_SCREENSHOTS_IDX, D_HIRESTEXTURES_IDX, + D_RIIVOLUTION_IDX, D_DUMP_IDX, D_DUMPFRAMES_IDX, D_DUMPOBJECTS_IDX, diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 3149a960f9..df7cf9014f 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -57,6 +57,9 @@ namespace fs = std::filesystem; #include "Core/PowerPC/PowerPC.h" #include "DiscIO/Enums.h" +#include "DiscIO/GameModDescriptor.h" +#include "DiscIO/RiivolutionParser.h" +#include "DiscIO/RiivolutionPatcher.h" #include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeWad.h" @@ -216,6 +219,31 @@ BootParameters::GenerateFromFile(std::vector paths, return std::make_unique(std::move(*wad), savestate_path); } + if (extension == ".json") + { + auto descriptor = DiscIO::ParseGameModDescriptorFile(path); + if (descriptor) + { + auto boot_params = GenerateFromFile(descriptor->base_file, savestate_path); + if (!boot_params) + { + PanicAlertFmtT("Could not recognize file {0}", descriptor->base_file); + return nullptr; + } + + if (descriptor->riivolution && std::holds_alternative(boot_params->parameters)) + { + const auto& volume = *std::get(boot_params->parameters).volume; + AddRiivolutionPatches(boot_params.get(), + DiscIO::Riivolution::GenerateRiivolutionPatchesFromGameModDescriptor( + *descriptor->riivolution, volume.GetGameID(), + volume.GetRevision(), volume.GetDiscNumber())); + } + + return boot_params; + } + } + PanicAlertFmtT("Could not recognize file {0}", path); return {}; } @@ -422,7 +450,10 @@ bool CBoot::BootUp(std::unique_ptr boot) struct BootTitle { - BootTitle() : config(SConfig::GetInstance()) {} + BootTitle(const std::vector& patches) + : config(SConfig::GetInstance()), riivolution_patches(patches) + { + } bool operator()(BootParameters::Disc& disc) const { NOTICE_LOG_FMT(BOOT, "Booting from disc: {}", disc.path); @@ -432,7 +463,7 @@ bool CBoot::BootUp(std::unique_ptr boot) if (!volume) return false; - if (!EmulatedBS2(config.bWii, *volume)) + if (!EmulatedBS2(config.bWii, *volume, riivolution_patches)) return false; SConfig::OnNewTitleLoad(); @@ -542,11 +573,14 @@ bool CBoot::BootUp(std::unique_ptr boot) private: const SConfig& config; + const std::vector& riivolution_patches; }; - if (!std::visit(BootTitle(), boot->parameters)) + if (!std::visit(BootTitle(boot->riivolution_patches), boot->parameters)) return false; + DiscIO::Riivolution::ApplyGeneralMemoryPatches(boot->riivolution_patches); + return true; } @@ -604,3 +638,20 @@ void CreateSystemMenuTitleDirs() const auto es = IOS::HLE::GetIOS()->GetES(); es->CreateTitleDirectories(Titles::SYSTEM_MENU, IOS::SYSMENU_GID); } + +void AddRiivolutionPatches(BootParameters* boot_params, + std::vector riivolution_patches) +{ + if (riivolution_patches.empty()) + return; + if (!std::holds_alternative(boot_params->parameters)) + return; + + auto& disc = std::get(boot_params->parameters); + disc.volume = DiscIO::CreateDisc(DiscIO::DirectoryBlobReader::Create( + std::move(disc.volume), + [&](std::vector* fst, DiscIO::FSTBuilderNode* dol_node) { + DiscIO::Riivolution::ApplyPatchesToFiles(riivolution_patches, fst, dol_node); + })); + boot_params->riivolution_patches = std::move(riivolution_patches); +} diff --git a/Source/Core/Core/Boot/Boot.h b/Source/Core/Core/Boot/Boot.h index 94fbe43ca3..d9a891966b 100644 --- a/Source/Core/Core/Boot/Boot.h +++ b/Source/Core/Core/Boot/Boot.h @@ -15,6 +15,7 @@ #include "Core/IOS/IOSC.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" +#include "DiscIO/RiivolutionParser.h" #include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeWad.h" @@ -78,6 +79,7 @@ struct BootParameters BootParameters(Parameters&& parameters_, const std::optional& savestate_path_ = {}); Parameters parameters; + std::vector riivolution_patches; std::optional savestate_path; bool delete_savestate = false; }; @@ -113,10 +115,14 @@ private: static void SetupMSR(); static void SetupBAT(bool is_wii); - static bool RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume); - static bool EmulatedBS2_GC(const DiscIO::VolumeDisc& volume); - static bool EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume); - static bool EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume); + static bool RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches); + static bool EmulatedBS2_GC(const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches); + static bool EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches); + static bool EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches); static bool Load_BS2(const std::string& boot_rom_filename); static void SetupGCMemory(); @@ -161,3 +167,6 @@ void UpdateStateFlags(std::function update_function); /// Normally, this is automatically done by ES when the System Menu is installed, /// but we cannot rely on this because we don't require any system titles to be installed. void CreateSystemMenuTitleDirs(); + +void AddRiivolutionPatches(BootParameters* boot_params, + std::vector riivolution_patches); diff --git a/Source/Core/Core/Boot/Boot_BS2Emu.cpp b/Source/Core/Core/Boot/Boot_BS2Emu.cpp index 556c2389c8..4a533cd6ed 100644 --- a/Source/Core/Core/Boot/Boot_BS2Emu.cpp +++ b/Source/Core/Core/Boot/Boot_BS2Emu.cpp @@ -33,6 +33,7 @@ #include "Core/PowerPC/PowerPC.h" #include "DiscIO/Enums.h" +#include "DiscIO/RiivolutionPatcher.h" #include "DiscIO/VolumeDisc.h" namespace @@ -87,7 +88,8 @@ void CBoot::SetupBAT(bool is_wii) PowerPC::IBATUpdated(); } -bool CBoot::RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume) +bool CBoot::RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches) { const DiscIO::Partition partition = volume.GetGamePartition(); @@ -148,6 +150,8 @@ bool CBoot::RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume) ram_address, length); DVDRead(volume, dvd_offset, ram_address, length, partition); + DiscIO::Riivolution::ApplyApploaderMemoryPatches(riivolution_patches, ram_address, length); + PowerPC::ppcState.gpr[3] = 0x81300004; PowerPC::ppcState.gpr[4] = 0x81300008; PowerPC::ppcState.gpr[5] = 0x8130000c; @@ -203,7 +207,8 @@ void CBoot::SetupGCMemory() // GameCube Bootstrap 2 HLE: // copy the apploader to 0x81200000 // execute the apploader, function by function, using the above utility. -bool CBoot::EmulatedBS2_GC(const DiscIO::VolumeDisc& volume) +bool CBoot::EmulatedBS2_GC(const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches) { INFO_LOG_FMT(BOOT, "Faking GC BS2..."); @@ -240,7 +245,7 @@ bool CBoot::EmulatedBS2_GC(const DiscIO::VolumeDisc& volume) // Global pointer to Small Data Area Base (Luigi's Mansion's apploader uses it) PowerPC::ppcState.gpr[13] = ntsc ? 0x81465320 : 0x814b4fc0; - return RunApploader(/*is_wii*/ false, volume); + return RunApploader(/*is_wii*/ false, volume, riivolution_patches); } static DiscIO::Region CodeRegion(char c) @@ -436,7 +441,8 @@ static void WriteEmptyPlayRecord() // Wii Bootstrap 2 HLE: // copy the apploader to 0x81200000 // execute the apploader -bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) +bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches) { INFO_LOG_FMT(BOOT, "Faking Wii BS2..."); if (volume.GetVolumeType() != DiscIO::Platform::WiiDisc) @@ -493,7 +499,7 @@ bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) PowerPC::ppcState.gpr[1] = 0x816ffff0; // StackPointer - if (!RunApploader(/*is_wii*/ true, volume)) + if (!RunApploader(/*is_wii*/ true, volume, riivolution_patches)) return false; // The Apploader probably just overwrote values needed for RAM Override. Run this again! @@ -508,7 +514,9 @@ bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) // Returns true if apploader has run successfully. If is_wii is true, the disc // that volume refers to must currently be inserted into the emulated disc drive. -bool CBoot::EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume) +bool CBoot::EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume, + const std::vector& riivolution_patches) { - return is_wii ? EmulatedBS2_Wii(volume) : EmulatedBS2_GC(volume); + return is_wii ? EmulatedBS2_Wii(volume, riivolution_patches) : + EmulatedBS2_GC(volume, riivolution_patches); } diff --git a/Source/Core/Core/BootManager.cpp b/Source/Core/Core/BootManager.cpp index 308cfbcd01..fca36916b3 100644 --- a/Source/Core/Core/BootManager.cpp +++ b/Source/Core/Core/BootManager.cpp @@ -418,6 +418,10 @@ bool BootCore(std::unique_ptr boot, const WindowSystemInfo& wsi) if (StartUp.bWii && DiscIO::IsNTSC(StartUp.m_region) && Config::Get(Config::SYSCONF_PAL60)) Config::SetCurrent(Config::SYSCONF_PAL60, false); + // Disable loading time emulation for Riivolution-patched games until we have proper emulation. + if (!boot->riivolution_patches.empty()) + StartUp.bFastDiscSpeed = true; + Core::UpdateWantDeterminism(/*initial*/ true); if (StartUp.bWii) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 518619a834..709c899e06 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -73,6 +73,8 @@ #include "Core/MemoryWatcher.h" #endif +#include "DiscIO/RiivolutionPatcher.h" + #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/GCAdapter.h" @@ -603,6 +605,10 @@ static void EmuThread(std::unique_ptr boot, WindowSystemInfo wsi else cpuThreadFunc = CpuThread; + std::optional savegame_redirect = std::nullopt; + if (SConfig::GetInstance().bWii) + savegame_redirect = DiscIO::Riivolution::ExtractSavegameRedirect(boot->riivolution_patches); + if (!CBoot::BootUp(std::move(boot))) return; @@ -611,7 +617,7 @@ static void EmuThread(std::unique_ptr boot, WindowSystemInfo wsi // with the correct title context since save copying requires title directories to exist. Common::ScopeGuard wiifs_guard{&Core::CleanUpWiiFileSystemContents}; if (SConfig::GetInstance().bWii) - Core::InitializeWiiFileSystemContents(); + Core::InitializeWiiFileSystemContents(savegame_redirect); else wiifs_guard.Dismiss(); diff --git a/Source/Core/Core/IOS/FS/FileSystem.h b/Source/Core/Core/IOS/FS/FileSystem.h index dea78a26e9..6b1d7ddf78 100644 --- a/Source/Core/Core/IOS/FS/FileSystem.h +++ b/Source/Core/Core/IOS/FS/FileSystem.h @@ -72,6 +72,15 @@ enum class SeekMode : u32 using FileAttribute = u8; +struct NandRedirect +{ + // A Wii FS path, eg. "/title/00010000/534d4e45/data". + std::string source_path; + + // An absolute host filesystem path the above should be redirected to. + std::string target_path; +}; + struct Modes { Mode owner, group, other; @@ -239,6 +248,8 @@ public: virtual Result GetNandStats() = 0; /// Get usage information about a directory (used cluster and inode counts). virtual Result GetDirectoryStats(const std::string& path) = 0; + + virtual void SetNandRedirects(std::vector nand_redirects) = 0; }; template @@ -269,7 +280,8 @@ enum class Location Session, }; -std::unique_ptr MakeFileSystem(Location location = Location::Session); +std::unique_ptr MakeFileSystem(Location location = Location::Session, + std::vector nand_redirects = {}); /// Convert a FS result code to an IOS error code. IOS::HLE::ReturnCode ConvertResult(ResultCode code); diff --git a/Source/Core/Core/IOS/FS/FileSystemCommon.cpp b/Source/Core/Core/IOS/FS/FileSystemCommon.cpp index bc904227ae..8d246157bb 100644 --- a/Source/Core/Core/IOS/FS/FileSystemCommon.cpp +++ b/Source/Core/Core/IOS/FS/FileSystemCommon.cpp @@ -28,11 +28,12 @@ SplitPathResult SplitPathAndBasename(std::string_view path) std::string(path.substr(last_separator + 1))}; } -std::unique_ptr MakeFileSystem(Location location) +std::unique_ptr MakeFileSystem(Location location, + std::vector nand_redirects) { const std::string nand_root = File::GetUserPath(location == Location::Session ? D_SESSION_WIIROOT_IDX : D_WIIROOT_IDX); - return std::make_unique(nand_root); + return std::make_unique(nand_root, std::move(nand_redirects)); } IOS::HLE::ReturnCode ConvertResult(ResultCode code) diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp index cdc8d2e7b2..7bdb785e7f 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp @@ -22,13 +22,24 @@ namespace IOS::HLE::FS { -std::string HostFileSystem::BuildFilename(const std::string& wii_path) const +HostFileSystem::HostFilename HostFileSystem::BuildFilename(const std::string& wii_path) const { + for (const auto& redirect : m_nand_redirects) + { + if (StringBeginsWith(wii_path, redirect.source_path) && + (wii_path.size() == redirect.source_path.size() || + wii_path[redirect.source_path.size()] == '/')) + { + std::string relative_to_redirect = wii_path.substr(redirect.source_path.size()); + return HostFilename{redirect.target_path + Common::EscapePath(relative_to_redirect), true}; + } + } + if (wii_path.compare(0, 1, "/") == 0) - return m_root_path + Common::EscapePath(wii_path); + return HostFilename{m_root_path + Common::EscapePath(wii_path), false}; ASSERT(false); - return m_root_path; + return HostFilename{m_root_path, false}; } // Get total filesize of contents of a directory (recursive) @@ -101,7 +112,9 @@ bool HostFileSystem::FstEntry::CheckPermission(Uid caller_uid, Gid caller_gid, return (u8(requested_mode) & u8(file_mode)) == u8(requested_mode); } -HostFileSystem::HostFileSystem(const std::string& root_path) : m_root_path{root_path} +HostFileSystem::HostFileSystem(const std::string& root_path, + std::vector nand_redirects) + : m_root_path{root_path}, m_nand_redirects(std::move(nand_redirects)) { File::CreateFullPath(m_root_path + "/"); ResetFst(); @@ -197,11 +210,12 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string& if (!IsValidNonRootPath(path)) return nullptr; - const File::FileInfo host_file_info{BuildFilename(path)}; + auto host_file = BuildFilename(path); + const File::FileInfo host_file_info{host_file.host_path}; if (!host_file_info.Exists()) return nullptr; - FstEntry* entry = &m_root_entry; + FstEntry* entry = host_file.is_redirect ? &m_redirect_fst : &m_root_entry; std::string complete_path = ""; for (const std::string& component : SplitString(std::string(path.substr(1)), '/')) { @@ -217,7 +231,8 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string& // Fall back to dummy data to avoid breaking existing filesystems. // This code path is also reached when creating a new file or directory; // proper metadata is filled in later. - INFO_LOG_FMT(IOS_FS, "Creating a default entry for {}", complete_path); + INFO_LOG_FMT(IOS_FS, "Creating a default entry for {} ({})", complete_path, + host_file.is_redirect ? "redirect" : "NAND"); entry = &entry->children.emplace_back(); entry->name = component; entry->data.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite}; @@ -241,7 +256,7 @@ void HostFileSystem::DoState(PointerWrap& p) handle.host_file.reset(); // handle /tmp - std::string Path = BuildFilename("/tmp"); + std::string Path = BuildFilename("/tmp").host_path; if (p.GetMode() == PointerWrap::MODE_READ) { File::DeleteDirRecursively(Path); @@ -336,7 +351,7 @@ void HostFileSystem::DoState(PointerWrap& p) p.Do(handle.wii_path); p.Do(handle.file_offset); if (handle.opened) - handle.host_file = OpenHostFile(BuildFilename(handle.wii_path)); + handle.host_file = OpenHostFile(BuildFilename(handle.wii_path).host_path); } } @@ -346,7 +361,7 @@ ResultCode HostFileSystem::Format(Uid uid) return ResultCode::AccessDenied; if (m_root_path.empty()) return ResultCode::AccessDenied; - const std::string root = BuildFilename("/"); + const std::string root = BuildFilename("/").host_path; if (!File::DeleteDirRecursively(root) || !File::CreateDir(root)) return ResultCode::UnknownError; ResetFst(); @@ -366,7 +381,7 @@ ResultCode HostFileSystem::CreateFileOrDirectory(Uid uid, Gid gid, const std::st return ResultCode::TooManyPathComponents; const auto split_path = SplitPathAndBasename(path); - const std::string host_path = BuildFilename(path); + const std::string host_path = BuildFilename(path).host_path; FstEntry* parent = GetFstEntryForPath(split_path.parent); if (!parent) @@ -428,7 +443,7 @@ ResultCode HostFileSystem::Delete(Uid uid, Gid gid, const std::string& path) if (!IsValidNonRootPath(path)) return ResultCode::Invalid; - const std::string host_path = BuildFilename(path); + const std::string host_path = BuildFilename(path).host_path; const auto split_path = SplitPathAndBasename(path); FstEntry* parent = GetFstEntryForPath(split_path.parent); @@ -491,8 +506,10 @@ ResultCode HostFileSystem::Rename(Uid uid, Gid gid, const std::string& old_path, return ResultCode::InUse; } - const std::string host_old_path = BuildFilename(old_path); - const std::string host_new_path = BuildFilename(new_path); + const auto host_old_info = BuildFilename(old_path); + const auto host_new_info = BuildFilename(new_path); + const std::string& host_old_path = host_old_info.host_path; + const std::string& host_new_path = host_new_info.host_path; // If there is already something of the same type at the new path, delete it. if (File::Exists(host_new_path)) @@ -509,8 +526,27 @@ ResultCode HostFileSystem::Rename(Uid uid, Gid gid, const std::string& old_path, if (!File::Rename(host_old_path, host_new_path)) { - ERROR_LOG_FMT(IOS_FS, "Rename {} to {} - failed", host_old_path, host_new_path); - return ResultCode::NotFound; + if (host_old_info.is_redirect || host_new_info.is_redirect) + { + // If either path is a redirect, the source and target may be on a different partition or + // device, so a simple rename may not work. Fall back to Copy & Delete and see if that works. + if (!File::Copy(host_old_path, host_new_path)) + { + ERROR_LOG_FMT(IOS_FS, "Copying {} to {} in Rename fallback failed", host_old_path, + host_new_path); + return ResultCode::NotFound; + } + if (!File::Delete(host_old_path)) + { + ERROR_LOG_FMT(IOS_FS, "Deleting {} in Rename fallback failed", host_old_path); + return ResultCode::Invalid; + } + } + else + { + ERROR_LOG_FMT(IOS_FS, "Rename {} to {} - failed", host_old_path, host_new_path); + return ResultCode::NotFound; + } } // Finally, remove the child from the old parent and move it to the new parent. @@ -544,7 +580,7 @@ Result> HostFileSystem::ReadDirectory(Uid uid, Gid gid, if (entry->data.is_file) return ResultCode::Invalid; - const std::string host_path = BuildFilename(path); + const std::string host_path = BuildFilename(path).host_path; File::FSTEntry host_entry = File::ScanDirectoryTree(host_path, false); for (File::FSTEntry& child : host_entry.children) { @@ -612,7 +648,7 @@ Result HostFileSystem::GetMetadata(Uid uid, Gid gid, const std::string return ResultCode::NotFound; Metadata metadata = entry->data; - metadata.size = File::GetSize(BuildFilename(path)); + metadata.size = File::GetSize(BuildFilename(path).host_path); return metadata; } @@ -631,7 +667,7 @@ ResultCode HostFileSystem::SetMetadata(Uid caller_uid, const std::string& path, if (caller_uid != 0 && uid != entry->data.uid) return ResultCode::AccessDenied; - const bool is_empty = File::GetSize(BuildFilename(path)) == 0; + const bool is_empty = File::GetSize(BuildFilename(path).host_path) == 0; if (entry->data.uid != uid && entry->data.is_file && !is_empty) return ResultCode::FileNotEmpty; @@ -667,7 +703,7 @@ Result HostFileSystem::GetDirectoryStats(const std::string& wii_ return ResultCode::Invalid; DirectoryStats stats{}; - std::string path(BuildFilename(wii_path)); + std::string path(BuildFilename(wii_path).host_path); if (File::IsDirectory(path)) { File::FSTEntry parent_dir = File::ScanDirectoryTree(path, true); @@ -685,4 +721,8 @@ Result HostFileSystem::GetDirectoryStats(const std::string& wii_ return stats; } +void HostFileSystem::SetNandRedirects(std::vector nand_redirects) +{ + m_nand_redirects = std::move(nand_redirects); +} } // namespace IOS::HLE::FS diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.h b/Source/Core/Core/IOS/FS/HostBackend/FS.h index d9d53669a3..17ef9baf36 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.h +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.h @@ -22,7 +22,7 @@ namespace IOS::HLE::FS class HostFileSystem final : public FileSystem { public: - HostFileSystem(const std::string& root_path); + HostFileSystem(const std::string& root_path, std::vector nand_redirects = {}); ~HostFileSystem(); void DoState(PointerWrap& p) override; @@ -56,6 +56,8 @@ public: Result GetNandStats() override; Result GetDirectoryStats(const std::string& path) override; + void SetNandRedirects(std::vector nand_redirects) override; + private: struct FstEntry { @@ -83,7 +85,12 @@ private: Handle* GetHandleFromFd(Fd fd); Fd ConvertHandleToFd(const Handle* handle) const; - std::string BuildFilename(const std::string& wii_path) const; + struct HostFilename + { + std::string host_path; + bool is_redirect; + }; + HostFilename BuildFilename(const std::string& wii_path) const; std::shared_ptr OpenHostFile(const std::string& host_path); ResultCode CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path, @@ -112,6 +119,9 @@ private: std::string m_root_path; std::map> m_open_files; std::array m_handles{}; + + FstEntry m_redirect_fst{}; + std::vector m_nand_redirects; }; } // namespace IOS::HLE::FS diff --git a/Source/Core/Core/IOS/FS/HostBackend/File.cpp b/Source/Core/Core/IOS/FS/HostBackend/File.cpp index c40f33dfaa..bdc4518310 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/File.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/File.cpp @@ -80,7 +80,7 @@ Result HostFileSystem::OpenFile(Uid, Gid, const std::string& path, M if (!handle) return ResultCode::NoFreeHandle; - const std::string host_path = BuildFilename(path); + const std::string host_path = BuildFilename(path).host_path; if (!File::IsFile(host_path)) { *handle = Handle{}; diff --git a/Source/Core/Core/IOS/IOS.cpp b/Source/Core/Core/IOS/IOS.cpp index 868e0476b5..53c99ddaf9 100644 --- a/Source/Core/Core/IOS/IOS.cpp +++ b/Source/Core/Core/IOS/IOS.cpp @@ -498,7 +498,7 @@ void Kernel::AddDevice(std::unique_ptr device) void Kernel::AddCoreDevices() { - m_fs = FS::MakeFileSystem(); + m_fs = FS::MakeFileSystem(IOS::HLE::FS::Location::Session, Core::GetActiveNandRedirects()); ASSERT(m_fs); std::lock_guard lock(m_device_map_mutex); diff --git a/Source/Core/Core/WiiRoot.cpp b/Source/Core/Core/WiiRoot.cpp index ea795ac138..d57f194ddc 100644 --- a/Source/Core/Core/WiiRoot.cpp +++ b/Source/Core/Core/WiiRoot.cpp @@ -4,6 +4,7 @@ #include "Core/WiiRoot.h" #include +#include #include #include @@ -34,6 +35,12 @@ namespace FS = IOS::HLE::FS; static std::string s_temp_wii_root; static bool s_wii_root_initialized = false; +static std::vector s_nand_redirects; + +const std::vector& GetActiveNandRedirects() +{ + return s_nand_redirects; +} static bool CopyBackupFile(const std::string& path_from, const std::string& path_to) { @@ -202,6 +209,7 @@ void InitializeWiiRoot(bool use_temporary) File::SetUserPath(D_SESSION_WIIROOT_IDX, File::GetUserPath(D_WIIROOT_IDX)); } + s_nand_redirects.clear(); s_wii_root_initialized = true; } @@ -213,6 +221,7 @@ void ShutdownWiiRoot() s_temp_wii_root.clear(); } + s_nand_redirects.clear(); s_wii_root_initialized = false; } @@ -288,7 +297,8 @@ static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_sou return true; } -void InitializeWiiFileSystemContents() +void InitializeWiiFileSystemContents( + std::optional save_redirect) { const auto fs = IOS::HLE::GetIOS()->GetFS(); @@ -299,14 +309,31 @@ 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()) - return; + if (WiiRootIsTemporary()) + { + // Generate a SYSCONF with default settings for the temporary Wii NAND. + SysConf sysconf{fs}; + sysconf.Save(); - // Generate a SYSCONF with default settings for the temporary Wii NAND. - SysConf sysconf{fs}; - sysconf.Save(); - - InitializeDeterministicWiiSaves(fs.get()); + InitializeDeterministicWiiSaves(fs.get()); + } + else if (save_redirect) + { + const u64 title_id = SConfig::GetInstance().GetTitleID(); + std::string source_path = Common::GetTitleDataPath(title_id); + 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), + save_redirect->m_target_path); + } + } + s_nand_redirects.emplace_back(IOS::HLE::FS::NandRedirect{ + std::move(source_path), std::move(save_redirect->m_target_path)}); + fs->SetNandRedirects(s_nand_redirects); + } } void CleanUpWiiFileSystemContents() diff --git a/Source/Core/Core/WiiRoot.h b/Source/Core/Core/WiiRoot.h index 2875cfa0cb..0b17061b19 100644 --- a/Source/Core/Core/WiiRoot.h +++ b/Source/Core/Core/WiiRoot.h @@ -3,6 +3,16 @@ #pragma once +#include +#include + +#include "DiscIO/RiivolutionPatcher.h" + +namespace IOS::HLE::FS +{ +struct NandRedirect; +} + namespace Core { enum class RestoreReason @@ -21,6 +31,9 @@ void BackupWiiSettings(); void RestoreWiiSettings(RestoreReason reason); // Initialize or clean up the filesystem contents. -void InitializeWiiFileSystemContents(); +void InitializeWiiFileSystemContents( + std::optional save_redirect); void CleanUpWiiFileSystemContents(); + +const std::vector& GetActiveNandRedirects(); } // namespace Core diff --git a/Source/Core/DiscIO/CMakeLists.txt b/Source/Core/DiscIO/CMakeLists.txt index 1729f0d959..cf13329f99 100644 --- a/Source/Core/DiscIO/CMakeLists.txt +++ b/Source/Core/DiscIO/CMakeLists.txt @@ -23,11 +23,17 @@ add_library(discio FileSystemGCWii.h Filesystem.cpp Filesystem.h + GameModDescriptor.cpp + GameModDescriptor.h LaggedFibonacciGenerator.cpp LaggedFibonacciGenerator.h MultithreadedCompressor.h NANDImporter.cpp NANDImporter.h + RiivolutionParser.cpp + RiivolutionParser.h + RiivolutionPatcher.cpp + RiivolutionPatcher.h ScrubbedBlob.cpp ScrubbedBlob.h TGCBlob.cpp diff --git a/Source/Core/DiscIO/DirectoryBlob.cpp b/Source/Core/DiscIO/DirectoryBlob.cpp index c8352c79e1..80bd173ae0 100644 --- a/Source/Core/DiscIO/DirectoryBlob.cpp +++ b/Source/Core/DiscIO/DirectoryBlob.cpp @@ -28,6 +28,7 @@ #include "Core/IOS/ES/Formats.h" #include "DiscIO/Blob.h" #include "DiscIO/DiscUtils.h" +#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeWii.h" #include "DiscIO/WiiEncryptionCache.h" @@ -40,9 +41,7 @@ static size_t ReadFileToVector(const std::string& path, std::vector* vector) static void PadToAddress(u64 start_address, u64* address, u64* length, u8** buffer); static void Write32(u32 data, u32 offset, std::vector* buffer); -static u32 ComputeNameSize(const File::FSTEntry& parent_entry); static std::string ASCIIToUppercase(std::string str); -static void ConvertUTF8NamesToSHIFTJIS(File::FSTEntry* parent_entry); enum class PartitionType : u32 { @@ -61,18 +60,8 @@ constexpr u8 ENTRY_SIZE = 0x0c; constexpr u8 FILE_ENTRY = 0; constexpr u8 DIRECTORY_ENTRY = 1; -DiscContent::DiscContent(u64 offset, u64 size, const std::string& path) - : m_offset(offset), m_size(size), m_content_source(path) -{ -} - -DiscContent::DiscContent(u64 offset, u64 size, const u8* data) - : m_offset(offset), m_size(size), m_content_source(data) -{ -} - -DiscContent::DiscContent(u64 offset, u64 size, DirectoryBlobReader* blob) - : m_offset(offset), m_size(size), m_content_source(blob) +DiscContent::DiscContent(u64 offset, u64 size, ContentSource source) + : m_offset(offset), m_size(size), m_content_source(std::move(source)) { } @@ -107,27 +96,51 @@ bool DiscContent::Read(u64* offset, u64* length, u8** buffer) const { const u64 bytes_to_read = std::min(m_size - offset_in_content, *length); - if (std::holds_alternative(m_content_source)) + if (std::holds_alternative(m_content_source)) { - File::IOFile file(std::get(m_content_source), "rb"); - if (!file.Seek(offset_in_content, SEEK_SET) || !file.ReadBytes(*buffer, bytes_to_read)) + const auto& content = std::get(m_content_source); + File::IOFile file(content.m_filename, "rb"); + if (!file.Seek(content.m_offset + offset_in_content, SEEK_SET) || + !file.ReadBytes(*buffer, bytes_to_read)) + { return false; + } } else if (std::holds_alternative(m_content_source)) { const u8* const content_pointer = std::get(m_content_source) + offset_in_content; std::copy(content_pointer, content_pointer + bytes_to_read, *buffer); } - else + else if (std::holds_alternative(m_content_source)) { - DirectoryBlobReader* blob = std::get(m_content_source); + const auto& content = std::get(m_content_source); + DirectoryBlobReader* blob = content.m_reader; const u64 decrypted_size = m_size * VolumeWii::BLOCK_DATA_SIZE / VolumeWii::BLOCK_TOTAL_SIZE; - if (!blob->EncryptPartitionData(offset_in_content, bytes_to_read, *buffer, m_offset, - decrypted_size)) + if (!blob->EncryptPartitionData(content.m_offset + offset_in_content, bytes_to_read, *buffer, + content.m_partition_data_offset, decrypted_size)) { return false; } } + else if (std::holds_alternative(m_content_source)) + { + const auto& source = std::get(m_content_source); + if (!source.m_volume->Read(source.m_offset + offset_in_content, bytes_to_read, *buffer, + source.m_partition)) + { + return false; + } + } + else if (std::holds_alternative(m_content_source)) + { + const ContentFixedByte& source = std::get(m_content_source); + std::fill_n(*buffer, bytes_to_read, source.m_byte); + } + else + { + PanicAlertFmt("DirectoryBlob: Invalid content source in DiscContent."); + return false; + } *length -= bytes_to_read; *buffer += bytes_to_read; @@ -137,35 +150,23 @@ bool DiscContent::Read(u64* offset, u64* length, u8** buffer) const return true; } -void DiscContentContainer::Add(u64 offset, u64 size, const std::string& path) +void DiscContentContainer::Add(u64 offset, u64 size, ContentSource source) { if (size != 0) - m_contents.emplace(offset, size, path); -} - -void DiscContentContainer::Add(u64 offset, u64 size, const u8* data) -{ - if (size != 0) - m_contents.emplace(offset, size, data); -} - -void DiscContentContainer::Add(u64 offset, u64 size, DirectoryBlobReader* blob) -{ - if (size != 0) - m_contents.emplace(offset, size, blob); + m_contents.emplace(offset, size, std::move(source)); } u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, const std::string& path) { const u64 size = File::GetSize(path); - Add(offset, size, path); + Add(offset, size, ContentFile{path, 0}); return size; } u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path) { const u64 size = std::min(File::GetSize(path), max_size); - Add(offset, size, path); + Add(offset, size, ContentFile{path, 0}); return size; } @@ -355,6 +356,18 @@ std::unique_ptr DirectoryBlobReader::Create(const std::stri return std::unique_ptr(new DirectoryBlobReader(partition_root, true_root)); } +std::unique_ptr DirectoryBlobReader::Create( + std::unique_ptr volume, + const std::function* fst_nodes, FSTBuilderNode* dol_node)>& + fst_callback) +{ + if (!volume) + return nullptr; + + return std::unique_ptr( + new DirectoryBlobReader(std::move(volume), fst_callback)); +} + DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root, const std::string& true_root) : m_encryption_cache(this) @@ -370,8 +383,8 @@ DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root, } else { - SetNonpartitionDiscHeader(game_partition.GetHeader(), game_partition_root); - SetWiiRegionData(game_partition_root); + SetNonpartitionDiscHeaderFromFile(game_partition.GetHeader(), game_partition_root); + SetWiiRegionDataFromFile(game_partition_root); std::vector partitions; partitions.emplace_back(std::move(game_partition), PartitionType::Game); @@ -399,6 +412,62 @@ DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root, } } +DirectoryBlobReader::DirectoryBlobReader( + std::unique_ptr volume, + const std::function* fst_nodes, FSTBuilderNode* dol_node)>& + fst_callback) + : m_encryption_cache(this), m_wrapped_volume(std::move(volume)) +{ + DirectoryBlobPartition game_partition( + m_wrapped_volume.get(), m_wrapped_volume->GetGamePartition(), std::nullopt, fst_callback); + m_is_wii = game_partition.IsWii(); + + if (!m_is_wii) + { + m_gamecube_pseudopartition = std::move(game_partition); + m_data_size = m_gamecube_pseudopartition.GetDataSize(); + m_encrypted = false; + } + else + { + std::vector header_bin(WII_NONPARTITION_DISCHEADER_SIZE); + if (!m_wrapped_volume->Read(WII_NONPARTITION_DISCHEADER_ADDRESS, + WII_NONPARTITION_DISCHEADER_SIZE, header_bin.data(), + PARTITION_NONE)) + { + header_bin.clear(); + } + SetNonpartitionDiscHeader(game_partition.GetHeader(), std::move(header_bin)); + + std::vector wii_region_data(WII_REGION_DATA_SIZE); + if (!m_wrapped_volume->Read(WII_REGION_DATA_ADDRESS, WII_REGION_DATA_SIZE, + wii_region_data.data(), PARTITION_NONE)) + { + wii_region_data.clear(); + } + SetWiiRegionData(wii_region_data, "volume"); + + std::vector partitions; + partitions.emplace_back(std::move(game_partition), PartitionType::Game); + + for (Partition partition : m_wrapped_volume->GetPartitions()) + { + if (partition == m_wrapped_volume->GetGamePartition()) + continue; + + auto type = m_wrapped_volume->GetPartitionType(partition); + if (type) + { + partitions.emplace_back( + DirectoryBlobPartition(m_wrapped_volume.get(), partition, m_is_wii, nullptr), + static_cast(*type)); + } + } + + SetPartitions(std::move(partitions)); + } +} + bool DirectoryBlobReader::Read(u64 offset, u64 length, u8* buffer) { if (offset + length > m_data_size) @@ -468,44 +537,70 @@ u64 DirectoryBlobReader::GetDataSize() const return m_data_size; } -void DirectoryBlobReader::SetNonpartitionDiscHeader(const std::vector& partition_header, - const std::string& game_partition_root) +void DirectoryBlobReader::SetNonpartitionDiscHeaderFromFile(const std::vector& partition_header, + const std::string& game_partition_root) { - m_disc_header_nonpartition.resize(WII_NONPARTITION_DISCHEADER_SIZE); + std::vector header_bin(WII_NONPARTITION_DISCHEADER_SIZE); const size_t header_bin_bytes_read = - ReadFileToVector(game_partition_root + "disc/header.bin", &m_disc_header_nonpartition); + ReadFileToVector(game_partition_root + "disc/header.bin", &header_bin); + header_bin.resize(header_bin_bytes_read); + SetNonpartitionDiscHeader(partition_header, std::move(header_bin)); +} + +void DirectoryBlobReader::SetNonpartitionDiscHeader(const std::vector& partition_header, + std::vector header_bin) +{ + const size_t header_bin_size = header_bin.size(); + m_disc_header_nonpartition = std::move(header_bin); + m_disc_header_nonpartition.resize(WII_NONPARTITION_DISCHEADER_SIZE); // If header.bin is missing or smaller than expected, use the content of sys/boot.bin instead - std::copy(partition_header.data() + header_bin_bytes_read, - partition_header.data() + m_disc_header_nonpartition.size(), - m_disc_header_nonpartition.data() + header_bin_bytes_read); + if (header_bin_size < m_disc_header_nonpartition.size()) + { + std::copy(partition_header.data() + header_bin_size, + partition_header.data() + m_disc_header_nonpartition.size(), + m_disc_header_nonpartition.data() + header_bin_size); + } // 0x60 and 0x61 are the only differences between the partition and non-partition headers - if (header_bin_bytes_read < 0x60) + if (header_bin_size < 0x60) m_disc_header_nonpartition[0x60] = 0; - if (header_bin_bytes_read < 0x61) + if (header_bin_size < 0x61) m_disc_header_nonpartition[0x61] = 0; m_encrypted = std::all_of(m_disc_header_nonpartition.data() + 0x60, m_disc_header_nonpartition.data() + 0x64, [](u8 x) { return x == 0; }); - m_nonpartition_contents.Add(WII_NONPARTITION_DISCHEADER_ADDRESS, m_disc_header_nonpartition); + m_nonpartition_contents.AddReference(WII_NONPARTITION_DISCHEADER_ADDRESS, + m_disc_header_nonpartition); } -void DirectoryBlobReader::SetWiiRegionData(const std::string& game_partition_root) +void DirectoryBlobReader::SetWiiRegionDataFromFile(const std::string& game_partition_root) +{ + std::vector wii_region_data(WII_REGION_DATA_SIZE); + const std::string region_bin_path = game_partition_root + "disc/region.bin"; + const size_t bytes_read = ReadFileToVector(region_bin_path, &wii_region_data); + wii_region_data.resize(bytes_read); + SetWiiRegionData(wii_region_data, region_bin_path); +} + +void DirectoryBlobReader::SetWiiRegionData(const std::vector& wii_region_data, + const std::string& log_path) { m_wii_region_data.resize(0x10, 0x00); - m_wii_region_data.resize(0x20, 0x80); + m_wii_region_data.resize(WII_REGION_DATA_SIZE, 0x80); Write32(INVALID_REGION, 0, &m_wii_region_data); - const std::string region_bin_path = game_partition_root + "disc/region.bin"; - const size_t bytes_read = ReadFileToVector(region_bin_path, &m_wii_region_data); - if (bytes_read < 0x4) - ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", region_bin_path); - else if (bytes_read < 0x20) - ERROR_LOG_FMT(DISCIO, "Couldn't read age ratings from {}", region_bin_path); + std::copy_n(wii_region_data.begin(), + std::min(wii_region_data.size(), WII_REGION_DATA_SIZE), + m_wii_region_data.begin()); - m_nonpartition_contents.Add(WII_REGION_DATA_ADDRESS, m_wii_region_data); + if (wii_region_data.size() < 0x4) + ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", log_path); + else if (wii_region_data.size() < 0x20) + ERROR_LOG_FMT(DISCIO, "Couldn't read age ratings from {}", log_path); + + m_nonpartition_contents.AddReference(WII_REGION_DATA_ADDRESS, m_wii_region_data); } void DirectoryBlobReader::SetPartitions(std::vector&& partitions) @@ -562,16 +657,17 @@ void DirectoryBlobReader::SetPartitions(std::vector&& partiti SetPartitionHeader(&partitions[i].partition, partition_address); const u64 data_size = partitions[i].partition.GetDataSize(); - m_partitions.emplace(partition_address + PARTITION_DATA_OFFSET, - std::move(partitions[i].partition)); - m_nonpartition_contents.Add(partition_address + PARTITION_DATA_OFFSET, data_size, this); + const u64 partition_data_offset = partition_address + PARTITION_DATA_OFFSET; + m_partitions.emplace(partition_data_offset, std::move(partitions[i].partition)); + m_nonpartition_contents.Add(partition_data_offset, data_size, + ContentPartition{this, 0, partition_data_offset}); const u64 unaligned_next_partition_address = VolumeWii::EncryptedPartitionOffsetToRawOffset( data_size, Partition(partition_address), PARTITION_DATA_OFFSET); partition_address = Common::AlignUp(unaligned_next_partition_address, 0x10000ull); } m_data_size = partition_address; - m_nonpartition_contents.Add(PARTITION_TABLE_ADDRESS, m_partition_table); + m_nonpartition_contents.AddReference(PARTITION_TABLE_ADDRESS, m_partition_table); } // This function sets the header that's shortly before the start of the encrypted @@ -582,27 +678,88 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition, constexpr u32 TMD_OFFSET = 0x2c0; constexpr u32 H3_OFFSET = 0x4000; + const std::optional& wrapped_partition = partition->GetWrappedPartition(); const std::string& partition_root = partition->GetRootDirectory(); - const u64 ticket_size = m_nonpartition_contents.CheckSizeAndAdd( - partition_address + WII_PARTITION_TICKET_ADDRESS, WII_PARTITION_TICKET_SIZE, - partition_root + "ticket.bin"); + u64 ticket_size; + if (wrapped_partition) + { + const auto& ticket = m_wrapped_volume->GetTicket(*wrapped_partition).GetBytes(); + auto& new_ticket = m_extra_data.emplace_back(ticket); + if (new_ticket.size() > WII_PARTITION_TICKET_SIZE) + new_ticket.resize(WII_PARTITION_TICKET_SIZE); + ticket_size = new_ticket.size(); + m_nonpartition_contents.AddReference(partition_address + WII_PARTITION_TICKET_ADDRESS, + new_ticket); + } + else + { + ticket_size = m_nonpartition_contents.CheckSizeAndAdd( + partition_address + WII_PARTITION_TICKET_ADDRESS, WII_PARTITION_TICKET_SIZE, + partition_root + "ticket.bin"); + } - const u64 tmd_size = m_nonpartition_contents.CheckSizeAndAdd( - partition_address + TMD_OFFSET, IOS::ES::MAX_TMD_SIZE, partition_root + "tmd.bin"); + u64 tmd_size; + if (wrapped_partition) + { + const auto& tmd = m_wrapped_volume->GetTMD(*wrapped_partition).GetBytes(); + auto& new_tmd = m_extra_data.emplace_back(tmd); + if (new_tmd.size() > IOS::ES::MAX_TMD_SIZE) + new_tmd.resize(IOS::ES::MAX_TMD_SIZE); + tmd_size = new_tmd.size(); + m_nonpartition_contents.AddReference(partition_address + TMD_OFFSET, new_tmd); + } + else + { + tmd_size = m_nonpartition_contents.CheckSizeAndAdd( + partition_address + TMD_OFFSET, IOS::ES::MAX_TMD_SIZE, partition_root + "tmd.bin"); + } const u64 cert_offset = Common::AlignUp(TMD_OFFSET + tmd_size, 0x20ull); const u64 max_cert_size = H3_OFFSET - cert_offset; - const u64 cert_size = m_nonpartition_contents.CheckSizeAndAdd( - partition_address + cert_offset, max_cert_size, partition_root + "cert.bin"); - m_nonpartition_contents.CheckSizeAndAdd(partition_address + H3_OFFSET, WII_PARTITION_H3_SIZE, - partition_root + "h3.bin"); + u64 cert_size; + if (wrapped_partition) + { + const auto& cert = m_wrapped_volume->GetCertificateChain(*wrapped_partition); + auto& new_cert = m_extra_data.emplace_back(cert); + if (new_cert.size() > max_cert_size) + new_cert.resize(max_cert_size); + cert_size = new_cert.size(); + m_nonpartition_contents.AddReference(partition_address + cert_offset, new_cert); + } + else + { + cert_size = m_nonpartition_contents.CheckSizeAndAdd(partition_address + cert_offset, + max_cert_size, partition_root + "cert.bin"); + } + + if (wrapped_partition) + { + if (m_wrapped_volume->IsEncryptedAndHashed()) + { + const std::optional offset = m_wrapped_volume->ReadSwappedAndShifted( + wrapped_partition->offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE); + if (offset) + { + auto& new_h3 = m_extra_data.emplace_back(WII_PARTITION_H3_SIZE); + if (m_wrapped_volume->Read(wrapped_partition->offset + *offset, new_h3.size(), + new_h3.data(), PARTITION_NONE)) + { + m_nonpartition_contents.AddReference(partition_address + H3_OFFSET, new_h3); + } + } + } + } + else + { + m_nonpartition_contents.CheckSizeAndAdd(partition_address + H3_OFFSET, WII_PARTITION_H3_SIZE, + partition_root + "h3.bin"); + } constexpr u32 PARTITION_HEADER_SIZE = 0x1c; const u64 data_size = Common::AlignUp(partition->GetDataSize(), 0x7c00) / 0x7c00 * 0x8000; - m_partition_headers.emplace_back(PARTITION_HEADER_SIZE); - std::vector& partition_header = m_partition_headers.back(); + std::vector& partition_header = m_extra_data.emplace_back(PARTITION_HEADER_SIZE); Write32(static_cast(tmd_size), 0x0, &partition_header); Write32(TMD_OFFSET >> 2, 0x4, &partition_header); Write32(static_cast(cert_size), 0x8, &partition_header); @@ -611,7 +768,8 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition, Write32(PARTITION_DATA_OFFSET >> 2, 0x14, &partition_header); Write32(static_cast(data_size >> 2), 0x18, &partition_header); - m_nonpartition_contents.Add(partition_address + WII_PARTITION_TICKET_SIZE, partition_header); + m_nonpartition_contents.AddReference(partition_address + WII_PARTITION_TICKET_SIZE, + partition_header); std::vector ticket_buffer(ticket_size); m_nonpartition_contents.Read(partition_address + WII_PARTITION_TICKET_ADDRESS, ticket_size, @@ -621,24 +779,116 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition, partition->SetKey(ticket.GetTitleKey()); } +static void GenerateBuilderNodesFromFileSystem(const DiscIO::VolumeDisc& volume, + const DiscIO::Partition& partition, + std::vector* nodes, + const FileInfo& parent_info) +{ + for (const FileInfo& file_info : parent_info) + { + if (file_info.IsDirectory()) + { + std::vector child_nodes; + GenerateBuilderNodesFromFileSystem(volume, partition, &child_nodes, file_info); + nodes->emplace_back(FSTBuilderNode{file_info.GetName(), file_info.GetTotalChildren(), + std::move(child_nodes)}); + } + else + { + std::vector source; + source.emplace_back(BuilderContentSource{ + 0, file_info.GetSize(), ContentVolume{file_info.GetOffset(), &volume, partition}}); + nodes->emplace_back( + FSTBuilderNode{file_info.GetName(), file_info.GetSize(), std::move(source)}); + } + } +} + DirectoryBlobPartition::DirectoryBlobPartition(const std::string& root_directory, std::optional is_wii) : m_root_directory(root_directory) { - SetDiscHeaderAndDiscType(is_wii); - SetBI2(); - BuildFST(SetDOL(SetApploader())); + SetDiscHeaderFromFile(m_root_directory + "sys/boot.bin"); + SetDiscType(is_wii); + SetBI2FromFile(m_root_directory + "sys/bi2.bin"); + const u64 dol_address = SetApploaderFromFile(m_root_directory + "sys/apploader.img"); + const u64 fst_address = SetDOLFromFile(m_root_directory + "sys/main.dol", dol_address); + BuildFSTFromFolder(m_root_directory + "files/", fst_address); } -void DirectoryBlobPartition::SetDiscHeaderAndDiscType(std::optional is_wii) +DirectoryBlobPartition::DirectoryBlobPartition( + DiscIO::VolumeDisc* volume, const DiscIO::Partition& partition, std::optional is_wii, + const std::function* fst_nodes, FSTBuilderNode* dol_node)>& + fst_callback) + : m_wrapped_partition(partition) +{ + std::vector disc_header(DISCHEADER_SIZE); + if (!volume->Read(DISCHEADER_ADDRESS, DISCHEADER_SIZE, disc_header.data(), partition)) + disc_header.clear(); + SetDiscHeader(std::move(disc_header)); + SetDiscType(is_wii); + + std::vector bi2(BI2_SIZE); + if (!volume->Read(BI2_ADDRESS, BI2_SIZE, bi2.data(), partition)) + bi2.clear(); + SetBI2(std::move(bi2)); + + std::vector apploader; + const auto apploader_size = GetApploaderSize(*volume, partition); + if (apploader_size) + { + apploader.resize(*apploader_size); + if (!volume->Read(APPLOADER_ADDRESS, *apploader_size, apploader.data(), partition)) + apploader.clear(); + } + const u64 new_dol_address = SetApploader(apploader, "apploader"); + + FSTBuilderNode dol_node{"main.dol", 0, {}}; + const auto dol_offset = GetBootDOLOffset(*volume, partition); + if (dol_offset) + { + const auto dol_size = GetBootDOLSize(*volume, partition, *dol_offset); + if (dol_size) + { + std::vector dol_contents; + dol_contents.emplace_back( + BuilderContentSource{0, *dol_size, ContentVolume{*dol_offset, volume, partition}}); + dol_node.m_size = *dol_size; + dol_node.m_content = std::move(dol_contents); + } + } + + std::vector nodes; + + const FileSystem* fs = volume->GetFileSystem(partition); + if (fs && fs->IsValid()) + GenerateBuilderNodesFromFileSystem(*volume, partition, &nodes, fs->GetRoot()); + + if (fst_callback) + fst_callback(&nodes, &dol_node); + + const u64 new_fst_address = SetDOL(std::move(dol_node), new_dol_address); + BuildFST(std::move(nodes), new_fst_address); +} + +void DirectoryBlobPartition::SetDiscHeaderFromFile(const std::string& boot_bin_path) { m_disc_header.resize(DISCHEADER_SIZE); - const std::string boot_bin_path = m_root_directory + "sys/boot.bin"; if (ReadFileToVector(boot_bin_path, &m_disc_header) < 0x20) ERROR_LOG_FMT(DISCIO, "{} doesn't exist or is too small", boot_bin_path); - m_contents.Add(DISCHEADER_ADDRESS, m_disc_header); + m_contents.AddReference(DISCHEADER_ADDRESS, m_disc_header); +} +void DirectoryBlobPartition::SetDiscHeader(std::vector boot_bin) +{ + m_disc_header = std::move(boot_bin); + m_disc_header.resize(DISCHEADER_SIZE); + m_contents.AddReference(DISCHEADER_ADDRESS, m_disc_header); +} + +void DirectoryBlobPartition::SetDiscType(std::optional is_wii) +{ if (is_wii.has_value()) { m_is_wii = *is_wii; @@ -648,44 +898,64 @@ void DirectoryBlobPartition::SetDiscHeaderAndDiscType(std::optional is_wii m_is_wii = Common::swap32(&m_disc_header[0x18]) == WII_DISC_MAGIC; const bool is_gc = Common::swap32(&m_disc_header[0x1c]) == GAMECUBE_DISC_MAGIC; if (m_is_wii == is_gc) - ERROR_LOG_FMT(DISCIO, "Couldn't detect disc type based on {}", boot_bin_path); + { + ERROR_LOG_FMT(DISCIO, "Couldn't detect disc type based on disc header; assuming {}", + m_is_wii ? "Wii" : "GameCube"); + } } m_address_shift = m_is_wii ? 2 : 0; } -void DirectoryBlobPartition::SetBI2() +void DirectoryBlobPartition::SetBI2FromFile(const std::string& bi2_path) { m_bi2.resize(BI2_SIZE); if (!m_is_wii) Write32(INVALID_REGION, 0x18, &m_bi2); - const std::string bi2_path = m_root_directory + "sys/bi2.bin"; const size_t bytes_read = ReadFileToVector(bi2_path, &m_bi2); if (!m_is_wii && bytes_read < 0x1C) ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", bi2_path); - m_contents.Add(BI2_ADDRESS, m_bi2); + m_contents.AddReference(BI2_ADDRESS, m_bi2); } -u64 DirectoryBlobPartition::SetApploader() +void DirectoryBlobPartition::SetBI2(std::vector bi2) +{ + const size_t bi2_size = bi2.size(); + m_bi2 = std::move(bi2); + m_bi2.resize(BI2_SIZE); + + if (!m_is_wii && bi2_size < 0x1C) + Write32(INVALID_REGION, 0x18, &m_bi2); + + m_contents.AddReference(BI2_ADDRESS, m_bi2); +} + +u64 DirectoryBlobPartition::SetApploaderFromFile(const std::string& path) +{ + File::IOFile file(path, "rb"); + std::vector apploader(file.GetSize()); + file.ReadBytes(apploader.data(), apploader.size()); + return SetApploader(std::move(apploader), path); +} + +u64 DirectoryBlobPartition::SetApploader(std::vector apploader, const std::string& log_path) { bool success = false; - const std::string path = m_root_directory + "sys/apploader.img"; - File::IOFile file(path, "rb"); - m_apploader.resize(file.GetSize()); - if (m_apploader.size() < 0x20 || !file.ReadBytes(m_apploader.data(), m_apploader.size())) + m_apploader = std::move(apploader); + if (m_apploader.size() < 0x20) { - ERROR_LOG_FMT(DISCIO, "{} couldn't be accessed or is too small", path); + ERROR_LOG_FMT(DISCIO, "{} couldn't be accessed or is too small", log_path); } else { const size_t apploader_size = 0x20 + Common::swap32(*(u32*)&m_apploader[0x14]) + Common::swap32(*(u32*)&m_apploader[0x18]); if (apploader_size != m_apploader.size()) - ERROR_LOG_FMT(DISCIO, "{} is the wrong size... Is it really an apploader?", path); + ERROR_LOG_FMT(DISCIO, "{} is the wrong size... Is it really an apploader?", log_path); else success = true; } @@ -697,15 +967,15 @@ u64 DirectoryBlobPartition::SetApploader() Write32(static_cast(-1), 0x10, &m_apploader); } - m_contents.Add(APPLOADER_ADDRESS, m_apploader); + m_contents.AddReference(APPLOADER_ADDRESS, m_apploader); // Return DOL address, 32 byte aligned (plus 32 byte padding) return Common::AlignUp(APPLOADER_ADDRESS + m_apploader.size() + 0x20, 0x20ull); } -u64 DirectoryBlobPartition::SetDOL(u64 dol_address) +u64 DirectoryBlobPartition::SetDOLFromFile(const std::string& path, u64 dol_address) { - const u64 dol_size = m_contents.CheckSizeAndAdd(dol_address, m_root_directory + "sys/main.dol"); + const u64 dol_size = m_contents.CheckSizeAndAdd(dol_address, path); Write32(static_cast(dol_address >> m_address_shift), 0x0420, &m_disc_header); @@ -713,16 +983,92 @@ u64 DirectoryBlobPartition::SetDOL(u64 dol_address) return Common::AlignUp(dol_address + dol_size + 0x20, 0x20ull); } -void DirectoryBlobPartition::BuildFST(u64 fst_address) +u64 DirectoryBlobPartition::SetDOL(FSTBuilderNode dol_node, u64 dol_address) +{ + for (auto& content : dol_node.GetFileContent()) + m_contents.Add(dol_address + content.m_offset, content.m_size, std::move(content.m_source)); + + Write32(static_cast(dol_address >> m_address_shift), 0x0420, &m_disc_header); + + // Return FST address, 32 byte aligned (plus 32 byte padding) + return Common::AlignUp(dol_address + dol_node.m_size + 0x20, 0x20ull); +} + +static std::vector ConvertFSTEntriesToBuilderNodes(const File::FSTEntry& parent) +{ + std::vector nodes; + nodes.reserve(parent.children.size()); + for (const File::FSTEntry& entry : parent.children) + { + std::variant, std::vector> content; + if (entry.isDirectory) + { + content = ConvertFSTEntriesToBuilderNodes(entry); + } + else + { + content = + std::vector{{0, entry.size, ContentFile{entry.physicalName, 0}}}; + } + + nodes.emplace_back(FSTBuilderNode{entry.virtualName, entry.size, std::move(content)}); + } + return nodes; +} + +void DirectoryBlobPartition::BuildFSTFromFolder(const std::string& fst_root_path, u64 fst_address) +{ + auto nodes = ConvertFSTEntriesToBuilderNodes(File::ScanDirectoryTree(fst_root_path, true)); + BuildFST(std::move(nodes), fst_address); +} + +static void ConvertUTF8NamesToSHIFTJIS(std::vector* fst) +{ + for (FSTBuilderNode& entry : *fst) + { + if (entry.IsFolder()) + ConvertUTF8NamesToSHIFTJIS(&entry.GetFolderContent()); + entry.m_filename = UTF8ToSHIFTJIS(entry.m_filename); + } +} + +static u32 ComputeNameSize(const std::vector& files) +{ + u32 name_size = 0; + for (const FSTBuilderNode& entry : files) + { + if (entry.IsFolder()) + name_size += ComputeNameSize(entry.GetFolderContent()); + name_size += static_cast(entry.m_filename.length() + 1); + } + return name_size; +} + +static size_t RecalculateFolderSizes(std::vector* fst) +{ + size_t size = 0; + for (FSTBuilderNode& entry : *fst) + { + ++size; + if (entry.IsFile()) + continue; + + entry.m_size = RecalculateFolderSizes(&entry.GetFolderContent()); + size += entry.m_size; + } + return size; +} + +void DirectoryBlobPartition::BuildFST(std::vector root_nodes, u64 fst_address) { m_fst_data.clear(); - File::FSTEntry rootEntry = File::ScanDirectoryTree(m_root_directory + "files/", true); + ConvertUTF8NamesToSHIFTJIS(&root_nodes); - ConvertUTF8NamesToSHIFTJIS(&rootEntry); + u32 name_table_size = Common::AlignUp(ComputeNameSize(root_nodes), 1ull << m_address_shift); - u32 name_table_size = Common::AlignUp(ComputeNameSize(rootEntry), 1ull << m_address_shift); - u64 total_entries = rootEntry.size + 1; // The root entry itself isn't counted in rootEntry.size + // 1 extra for the root entry + u64 total_entries = RecalculateFolderSizes(&root_nodes) + 1; const u64 name_table_offset = total_entries * ENTRY_SIZE; m_fst_data.resize(name_table_offset + name_table_size); @@ -737,7 +1083,7 @@ void DirectoryBlobPartition::BuildFST(u64 fst_address) // write root entry WriteEntryData(&fst_offset, DIRECTORY_ENTRY, 0, 0, total_entries, m_address_shift); - WriteDirectory(rootEntry, &fst_offset, &name_offset, ¤t_data_address, root_offset, + WriteDirectory(&root_nodes, &fst_offset, &name_offset, ¤t_data_address, root_offset, name_table_offset); // overflow check, compare the aligned name offset with the aligned name table size @@ -748,7 +1094,7 @@ void DirectoryBlobPartition::BuildFST(u64 fst_address) Write32((u32)(m_fst_data.size() >> m_address_shift), 0x0428, &m_disc_header); Write32((u32)(m_fst_data.size() >> m_address_shift), 0x042c, &m_disc_header); - m_contents.Add(fst_address, m_fst_data); + m_contents.AddReference(fst_address, m_fst_data); m_data_size = current_data_address; } @@ -777,44 +1123,51 @@ void DirectoryBlobPartition::WriteEntryName(u32* name_offset, const std::string& *name_offset += (u32)(name.length() + 1); } -void DirectoryBlobPartition::WriteDirectory(const File::FSTEntry& parent_entry, u32* fst_offset, - u32* name_offset, u64* data_offset, +void DirectoryBlobPartition::WriteDirectory(std::vector* parent_entries, + u32* fst_offset, u32* name_offset, u64* data_offset, u32 parent_entry_index, u64 name_table_offset) { - std::vector sorted_entries = parent_entry.children; + std::vector& sorted_entries = *parent_entries; // Sort for determinism std::sort(sorted_entries.begin(), sorted_entries.end(), - [](const File::FSTEntry& one, const File::FSTEntry& two) { - const std::string one_upper = ASCIIToUppercase(one.virtualName); - const std::string two_upper = ASCIIToUppercase(two.virtualName); - return one_upper == two_upper ? one.virtualName < two.virtualName : + [](const FSTBuilderNode& one, const FSTBuilderNode& two) { + const std::string one_upper = ASCIIToUppercase(one.m_filename); + const std::string two_upper = ASCIIToUppercase(two.m_filename); + return one_upper == two_upper ? one.m_filename < two.m_filename : one_upper < two_upper; }); - for (const File::FSTEntry& entry : sorted_entries) + for (FSTBuilderNode& entry : sorted_entries) { - if (entry.isDirectory) + if (entry.IsFolder()) { u32 entry_index = *fst_offset / ENTRY_SIZE; WriteEntryData(fst_offset, DIRECTORY_ENTRY, *name_offset, parent_entry_index, - entry_index + entry.size + 1, 0); - WriteEntryName(name_offset, entry.virtualName, name_table_offset); + entry_index + entry.m_size + 1, 0); + WriteEntryName(name_offset, entry.m_filename, name_table_offset); - WriteDirectory(entry, fst_offset, name_offset, data_offset, entry_index, name_table_offset); + auto& child_nodes = entry.GetFolderContent(); + WriteDirectory(&child_nodes, fst_offset, name_offset, data_offset, entry_index, + name_table_offset); } else { // put entry in FST - WriteEntryData(fst_offset, FILE_ENTRY, *name_offset, *data_offset, entry.size, + WriteEntryData(fst_offset, FILE_ENTRY, *name_offset, *data_offset, entry.m_size, m_address_shift); - WriteEntryName(name_offset, entry.virtualName, name_table_offset); + WriteEntryName(name_offset, entry.m_filename, name_table_offset); // write entry to virtual disc - m_contents.Add(*data_offset, entry.size, entry.physicalName); + auto& contents = entry.GetFileContent(); + for (BuilderContentSource& content : contents) + { + m_contents.Add(*data_offset + content.m_offset, content.m_size, + std::move(content.m_source)); + } // 32 KiB aligned - many games are fine with less alignment, but not all - *data_offset = Common::AlignUp(*data_offset + entry.size, 0x8000ull); + *data_offset = Common::AlignUp(*data_offset + entry.m_size, 0x8000ull); } } } @@ -847,30 +1200,6 @@ static void Write32(u32 data, u32 offset, std::vector* buffer) (*buffer)[offset] = data & 0xff; } -static u32 ComputeNameSize(const File::FSTEntry& parent_entry) -{ - u32 name_size = 0; - for (const File::FSTEntry& entry : parent_entry.children) - { - if (entry.isDirectory) - name_size += ComputeNameSize(entry); - - name_size += (u32)entry.virtualName.length() + 1; - } - return name_size; -} - -static void ConvertUTF8NamesToSHIFTJIS(File::FSTEntry* parent_entry) -{ - for (File::FSTEntry& entry : parent_entry->children) - { - if (entry.isDirectory) - ConvertUTF8NamesToSHIFTJIS(&entry); - - entry.virtualName = UTF8ToSHIFTJIS(entry.virtualName); - } -} - static std::string ASCIIToUppercase(std::string str) { std::transform(str.begin(), str.end(), str.begin(), diff --git a/Source/Core/DiscIO/DirectoryBlob.h b/Source/Core/DiscIO/DirectoryBlob.h index e4eb1b3cd3..766ead1a7a 100644 --- a/Source/Core/DiscIO/DirectoryBlob.h +++ b/Source/Core/DiscIO/DirectoryBlob.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include "Common/CommonTypes.h" #include "Common/FileUtil.h" #include "DiscIO/Blob.h" +#include "DiscIO/Volume.h" #include "DiscIO/WiiEncryptionCache.h" namespace File @@ -29,22 +31,108 @@ namespace DiscIO enum class PartitionType : u32; class DirectoryBlobReader; +class VolumeDisc; // Returns true if the path is inside a DirectoryBlob and doesn't represent the DirectoryBlob itself bool ShouldHideFromGameList(const std::string& volume_path); +// Content chunk that is loaded from a file in the host file system. +struct ContentFile +{ + // Path where the file can be found. + std::string m_filename; + + // Offset from the start of the file where the first byte of this content chunk is. + u64 m_offset; +}; + +// Content chunk that loads data from a DirectoryBlobReader. +// Intented for representing a partition within a disc. +struct ContentPartition +{ + // The reader to read data from. + DirectoryBlobReader* m_reader; + + // Offset from the start of the partition for the first byte represented by this chunk. + u64 m_offset; + + // The value passed as partition_data_offset to EncryptPartitionData(). + u64 m_partition_data_offset; +}; + +// Content chunk that loads data from a Volume. +struct ContentVolume +{ + // Offset from the start of the volume for the first byte represented by this chunk. + u64 m_offset; + + // The volume to read data from. + const Volume* m_volume; + + // The partition passed to the Volume's Read() method. + Partition m_partition; +}; + +// Content chunk representing a run of identical bytes. +// Useful for padding between chunks within a file. +struct ContentFixedByte +{ + u8 m_byte; +}; + +using ContentSource = std::variant; + +struct BuilderContentSource +{ + u64 m_offset; + u64 m_size; + ContentSource m_source; +}; + +struct FSTBuilderNode +{ + std::string m_filename; + u64 m_size; + std::variant, std::vector> m_content; + void* m_user_data = nullptr; + + bool IsFile() const + { + return std::holds_alternative>(m_content); + } + + std::vector& GetFileContent() + { + return std::get>(m_content); + } + + const std::vector& GetFileContent() const + { + return std::get>(m_content); + } + + bool IsFolder() const { return std::holds_alternative>(m_content); } + + std::vector& GetFolderContent() + { + return std::get>(m_content); + } + + const std::vector& GetFolderContent() const + { + return std::get>(m_content); + } +}; + class DiscContent { public: - using ContentSource = - std::variant; - - DiscContent(u64 offset, u64 size, const std::string& path); - DiscContent(u64 offset, u64 size, const u8* data); - DiscContent(u64 offset, u64 size, DirectoryBlobReader* blob); + DiscContent(u64 offset, u64 size, ContentSource source); // Provided because it's convenient when searching for DiscContent in an std::set explicit DiscContent(u64 offset); @@ -62,8 +150,13 @@ public: bool operator>=(const DiscContent& other) const { return !(*this < other); } private: + // Position of this content chunk within its parent DiscContentContainer. u64 m_offset; + + // Number of bytes this content chunk takes up. u64 m_size = 0; + + // Where and how to find the data for this content chunk. ContentSource m_content_source; }; @@ -71,13 +164,11 @@ class DiscContentContainer { public: template - void Add(u64 offset, const std::vector& vector) + void AddReference(u64 offset, const std::vector& vector) { return Add(offset, vector.size() * sizeof(T), reinterpret_cast(vector.data())); } - void Add(u64 offset, u64 size, const std::string& path); - void Add(u64 offset, u64 size, const u8* data); - void Add(u64 offset, u64 size, DirectoryBlobReader* blob); + void Add(u64 offset, u64 size, ContentSource source); u64 CheckSizeAndAdd(u64 offset, const std::string& path); u64 CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path); @@ -92,6 +183,10 @@ class DirectoryBlobPartition public: DirectoryBlobPartition() = default; DirectoryBlobPartition(const std::string& root_directory, std::optional is_wii); + DirectoryBlobPartition(DiscIO::VolumeDisc* volume, const DiscIO::Partition& partition, + std::optional is_wii, + const std::function* fst_nodes, + FSTBuilderNode* dol_node)>& fst_callback); // We do not allow copying, because it might mess up the pointers inside DiscContents DirectoryBlobPartition(const DirectoryBlobPartition&) = delete; @@ -104,27 +199,38 @@ public: const std::string& GetRootDirectory() const { return m_root_directory; } const std::vector& GetHeader() const { return m_disc_header; } const DiscContentContainer& GetContents() const { return m_contents; } + const std::optional& GetWrappedPartition() const + { + return m_wrapped_partition; + } const std::array& GetKey() const { return m_key; } void SetKey(std::array key) { m_key = key; } private: - void SetDiscHeaderAndDiscType(std::optional is_wii); - void SetBI2(); + void SetDiscHeaderFromFile(const std::string& boot_bin_path); + void SetDiscHeader(std::vector boot_bin); + void SetDiscType(std::optional is_wii); + void SetBI2FromFile(const std::string& bi2_path); + void SetBI2(std::vector bi2); // Returns DOL address - u64 SetApploader(); + u64 SetApploaderFromFile(const std::string& path); + u64 SetApploader(std::vector apploader, const std::string& log_path); // Returns FST address - u64 SetDOL(u64 dol_address); + u64 SetDOLFromFile(const std::string& path, u64 dol_address); + u64 SetDOL(FSTBuilderNode dol_node, u64 dol_address); - void BuildFST(u64 fst_address); + void BuildFSTFromFolder(const std::string& fst_root_path, u64 fst_address); + void BuildFST(std::vector root_nodes, u64 fst_address); // FST creation void WriteEntryData(u32* entry_offset, u8 type, u32 name_offset, u64 data_offset, u64 length, u32 address_shift); void WriteEntryName(u32* name_offset, const std::string& name, u64 name_table_offset); - void WriteDirectory(const File::FSTEntry& parent_entry, u32* fst_offset, u32* name_offset, - u64* data_offset, u32 parent_entry_index, u64 name_table_offset); + void WriteDirectory(std::vector* parent_entries, u32* fst_offset, + u32* name_offset, u64* data_offset, u32 parent_entry_index, + u64 name_table_offset); DiscContentContainer m_contents; std::vector m_disc_header; @@ -140,6 +246,8 @@ private: u32 m_address_shift = 0; u64 m_data_size = 0; + + std::optional m_wrapped_partition = std::nullopt; }; class DirectoryBlobReader : public BlobReader @@ -148,6 +256,10 @@ class DirectoryBlobReader : public BlobReader public: static std::unique_ptr Create(const std::string& dol_path); + static std::unique_ptr Create( + std::unique_ptr volume, + const std::function* fst_nodes, FSTBuilderNode* dol_node)>& + fst_callback); // We do not allow copying, because it might mess up the pointers inside DiscContents DirectoryBlobReader(const DirectoryBlobReader&) = delete; @@ -183,15 +295,21 @@ private: explicit DirectoryBlobReader(const std::string& game_partition_root, const std::string& true_root); + explicit DirectoryBlobReader(std::unique_ptr volume, + const std::function* fst_nodes, + FSTBuilderNode* dol_node)>& fst_callback); const DirectoryBlobPartition* GetPartition(u64 offset, u64 size, u64 partition_data_offset) const; bool EncryptPartitionData(u64 offset, u64 size, u8* buffer, u64 partition_data_offset, u64 partition_data_decrypted_size); + void SetNonpartitionDiscHeaderFromFile(const std::vector& partition_header, + const std::string& game_partition_root); void SetNonpartitionDiscHeader(const std::vector& partition_header, - const std::string& game_partition_root); - void SetWiiRegionData(const std::string& game_partition_root); + std::vector header_bin); + void SetWiiRegionDataFromFile(const std::string& game_partition_root); + void SetWiiRegionData(const std::vector& wii_region_data, const std::string& log_path); void SetPartitions(std::vector&& partitions); void SetPartitionHeader(DirectoryBlobPartition* partition, u64 partition_address); @@ -209,9 +327,11 @@ private: std::vector m_disc_header_nonpartition; std::vector m_partition_table; std::vector m_wii_region_data; - std::vector> m_partition_headers; + std::vector> m_extra_data; u64 m_data_size; + + std::unique_ptr m_wrapped_volume; }; } // namespace DiscIO diff --git a/Source/Core/DiscIO/GameModDescriptor.cpp b/Source/Core/DiscIO/GameModDescriptor.cpp new file mode 100644 index 0000000000..4a1c479fca --- /dev/null +++ b/Source/Core/DiscIO/GameModDescriptor.cpp @@ -0,0 +1,147 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "DiscIO/GameModDescriptor.h" + +#include + +#include "Common/IOFile.h" +#include "Common/MathUtil.h" +#include "Common/StringUtil.h" + +namespace DiscIO +{ +static std::string MakeAbsolute(const std::string& directory, const std::string& path) +{ +#ifdef _WIN32 + return PathToString(StringToPath(directory) / StringToPath(path)); +#else + if (StringBeginsWith(path, "/")) + return path; + return directory + "/" + path; +#endif +} + +std::optional ParseGameModDescriptorFile(const std::string& filename) +{ + ::File::IOFile f(filename, "rb"); + if (!f) + return std::nullopt; + + std::vector data; + data.resize(f.GetSize()); + if (!f.ReadBytes(data.data(), data.size())) + return std::nullopt; + +#ifdef _WIN32 + std::string path = ReplaceAll(filename, "\\", "/"); +#else + const std::string& path = filename; +#endif + return ParseGameModDescriptorString(std::string_view(data.data(), data.size()), path); +} + +static std::vector +ParseRiivolutionOptions(const picojson::array& array) +{ + std::vector options; + for (const auto& option_object : array) + { + if (!option_object.is()) + continue; + + auto& option = options.emplace_back(); + for (const auto& [key, value] : option_object.get()) + { + if (key == "section-name" && value.is()) + option.section_name = value.get(); + else if (key == "option-id" && value.is()) + option.option_id = value.get(); + else if (key == "option-name" && value.is()) + option.option_name = value.get(); + else if (key == "choice" && value.is()) + option.choice = MathUtil::SaturatingCast(value.get()); + } + } + return options; +} + +static GameModDescriptorRiivolution ParseRiivolutionObject(const std::string& json_directory, + const picojson::object& object) +{ + GameModDescriptorRiivolution r; + for (const auto& [element_key, element_value] : object) + { + if (element_key == "patches" && element_value.is()) + { + for (const auto& patch_object : element_value.get()) + { + if (!patch_object.is()) + continue; + + auto& patch = r.patches.emplace_back(); + for (const auto& [key, value] : patch_object.get()) + { + if (key == "xml" && value.is()) + patch.xml = MakeAbsolute(json_directory, value.get()); + else if (key == "root" && value.is()) + patch.root = MakeAbsolute(json_directory, value.get()); + else if (key == "options" && value.is()) + patch.options = ParseRiivolutionOptions(value.get()); + } + } + } + } + return r; +} + +std::optional ParseGameModDescriptorString(std::string_view json, + std::string_view json_path) +{ + std::string json_directory; + SplitPath(json_path, &json_directory, nullptr, nullptr); + + picojson::value json_root; + std::string err; + picojson::parse(json_root, json.begin(), json.end(), &err); + if (!err.empty()) + return std::nullopt; + if (!json_root.is()) + return std::nullopt; + + GameModDescriptor descriptor; + bool is_valid_version = false; + for (const auto& [key, value] : json_root.get()) + { + if (key == "version" && value.is()) + { + is_valid_version = value.get() == 1.0; + } + else if (key == "base-file" && value.is()) + { + descriptor.base_file = MakeAbsolute(json_directory, value.get()); + } + else if (key == "display-name" && value.is()) + { + descriptor.display_name = value.get(); + } + else if (key == "banner" && value.is()) + { + descriptor.banner = MakeAbsolute(json_directory, value.get()); + } + else if (key == "riivolution" && value.is()) + { + descriptor.riivolution = + ParseRiivolutionObject(json_directory, value.get()); + } + } + if (!is_valid_version) + return std::nullopt; + return descriptor; +} +} // namespace DiscIO diff --git a/Source/Core/DiscIO/GameModDescriptor.h b/Source/Core/DiscIO/GameModDescriptor.h new file mode 100644 index 0000000000..09a03ed2df --- /dev/null +++ b/Source/Core/DiscIO/GameModDescriptor.h @@ -0,0 +1,46 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" + +namespace DiscIO +{ +struct GameModDescriptorRiivolutionPatchOption +{ + std::string section_name; + std::string option_id; + std::string option_name; + u32 choice = 0; +}; + +struct GameModDescriptorRiivolutionPatch +{ + std::string xml; + std::string root; + std::vector options; +}; + +struct GameModDescriptorRiivolution +{ + std::vector patches; +}; + +struct GameModDescriptor +{ + std::string base_file; + std::string display_name; + std::string banner; + std::optional riivolution = std::nullopt; +}; + +std::optional ParseGameModDescriptorFile(const std::string& filename); +std::optional ParseGameModDescriptorString(std::string_view json, + std::string_view json_path); +} // namespace DiscIO diff --git a/Source/Core/DiscIO/RiivolutionParser.cpp b/Source/Core/DiscIO/RiivolutionParser.cpp new file mode 100644 index 0000000000..92f37444a0 --- /dev/null +++ b/Source/Core/DiscIO/RiivolutionParser.cpp @@ -0,0 +1,486 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DiscIO/RiivolutionParser.h" + +#include +#include +#include +#include +#include + +#include + +#include "Common/FileUtil.h" +#include "Common/IOFile.h" +#include "Common/StringUtil.h" +#include "DiscIO/GameModDescriptor.h" +#include "DiscIO/RiivolutionPatcher.h" + +namespace DiscIO::Riivolution +{ +Patch::~Patch() = default; + +std::optional ParseFile(const std::string& filename) +{ + ::File::IOFile f(filename, "rb"); + if (!f) + return std::nullopt; + + std::vector data; + data.resize(f.GetSize()); + if (!f.ReadBytes(data.data(), data.size())) + return std::nullopt; + + return ParseString(std::string_view(data.data(), data.size()), filename); +} + +static std::map ReadParams(const pugi::xml_node& node, + std::map params = {}) +{ + for (const auto& param_node : node.children("param")) + { + const std::string param_name = param_node.attribute("name").as_string(); + const std::string param_value = param_node.attribute("value").as_string(); + params[param_name] = param_value; + } + return params; +} + +static std::vector ReadHexString(std::string_view sv) +{ + if ((sv.size() % 2) == 1) + return {}; + if (StringBeginsWith(sv, "0x") || StringBeginsWith(sv, "0X")) + sv = sv.substr(2); + + std::vector result; + result.reserve(sv.size() / 2); + while (!sv.empty()) + { + u8 tmp; + if (!TryParse(std::string(sv.substr(0, 2)), &tmp, 16)) + return {}; + result.push_back(tmp); + sv = sv.substr(2); + } + return result; +}; + +std::optional ParseString(std::string_view xml, std::string xml_path) +{ + pugi::xml_document doc; + const auto parse_result = doc.load_buffer(xml.data(), xml.size()); + if (!parse_result) + return std::nullopt; + + const auto wiidisc = doc.child("wiidisc"); + if (!wiidisc) + return std::nullopt; + + Disc disc; + disc.m_xml_path = std::move(xml_path); + disc.m_version = wiidisc.attribute("version").as_int(-1); + if (disc.m_version != 1) + return std::nullopt; + const std::string default_root = wiidisc.attribute("root").as_string(); + + const auto id = wiidisc.child("id"); + if (id) + { + for (const auto& attribute : id.attributes()) + { + const std::string_view attribute_name(attribute.name()); + if (attribute_name == "game") + disc.m_game_filter.m_game = attribute.as_string(); + else if (attribute_name == "developer") + disc.m_game_filter.m_developer = attribute.as_string(); + else if (attribute_name == "disc") + disc.m_game_filter.m_disc = attribute.as_int(-1); + else if (attribute_name == "version") + disc.m_game_filter.m_version = attribute.as_int(-1); + } + + auto xml_regions = id.children("region"); + if (xml_regions.begin() != xml_regions.end()) + { + std::vector regions; + for (const auto& region : xml_regions) + regions.push_back(region.attribute("type").as_string()); + disc.m_game_filter.m_regions = std::move(regions); + } + } + + const auto options = wiidisc.child("options"); + if (options) + { + for (const auto& section_node : options.children("section")) + { + Section& section = disc.m_sections.emplace_back(); + section.m_name = section_node.attribute("name").as_string(); + for (const auto& option_node : section_node.children("option")) + { + Option& option = section.m_options.emplace_back(); + option.m_id = option_node.attribute("id").as_string(); + option.m_name = option_node.attribute("name").as_string(); + option.m_selected_choice = option_node.attribute("default").as_uint(0); + auto option_params = ReadParams(option_node); + for (const auto& choice_node : option_node.children("choice")) + { + Choice& choice = option.m_choices.emplace_back(); + choice.m_name = choice_node.attribute("name").as_string(); + auto choice_params = ReadParams(choice_node, option_params); + for (const auto& patchref_node : choice_node.children("patch")) + { + PatchReference& patchref = choice.m_patch_references.emplace_back(); + patchref.m_id = patchref_node.attribute("id").as_string(); + patchref.m_params = ReadParams(patchref_node, choice_params); + } + } + } + } + for (const auto& macro_node : options.children("macros")) + { + const std::string macro_id = macro_node.attribute("id").as_string(); + for (auto& section : disc.m_sections) + { + auto option_to_clone = std::find_if(section.m_options.begin(), section.m_options.end(), + [&](const Option& o) { return o.m_id == macro_id; }); + if (option_to_clone == section.m_options.end()) + continue; + + Option cloned_option = *option_to_clone; + cloned_option.m_name = macro_node.attribute("name").as_string(); + for (auto& choice : cloned_option.m_choices) + for (auto& patch_ref : choice.m_patch_references) + patch_ref.m_params = ReadParams(macro_node, patch_ref.m_params); + } + } + } + + const auto patches = wiidisc.children("patch"); + for (const auto& patch_node : patches) + { + Patch& patch = disc.m_patches.emplace_back(); + patch.m_id = patch_node.attribute("id").as_string(); + patch.m_root = patch_node.attribute("root").as_string(); + if (patch.m_root.empty()) + patch.m_root = default_root; + + for (const auto& patch_subnode : patch_node.children()) + { + const std::string_view patch_name(patch_subnode.name()); + if (patch_name == "file") + { + auto& file = patch.m_file_patches.emplace_back(); + file.m_disc = patch_subnode.attribute("disc").as_string(); + file.m_external = patch_subnode.attribute("external").as_string(); + file.m_resize = patch_subnode.attribute("resize").as_bool(true); + file.m_create = patch_subnode.attribute("create").as_bool(false); + file.m_offset = patch_subnode.attribute("offset").as_uint(0); + file.m_fileoffset = patch_subnode.attribute("fileoffset").as_uint(0); + file.m_length = patch_subnode.attribute("length").as_uint(0); + } + else if (patch_name == "folder") + { + auto& folder = patch.m_folder_patches.emplace_back(); + folder.m_disc = patch_subnode.attribute("disc").as_string(); + folder.m_external = patch_subnode.attribute("external").as_string(); + folder.m_resize = patch_subnode.attribute("resize").as_bool(true); + folder.m_create = patch_subnode.attribute("create").as_bool(false); + folder.m_recursive = patch_subnode.attribute("recursive").as_bool(true); + folder.m_length = patch_subnode.attribute("length").as_uint(0); + } + else if (patch_name == "savegame") + { + auto& savegame = patch.m_savegame_patches.emplace_back(); + savegame.m_external = patch_subnode.attribute("external").as_string(); + savegame.m_clone = patch_subnode.attribute("clone").as_bool(true); + } + else if (patch_name == "memory") + { + auto& memory = patch.m_memory_patches.emplace_back(); + memory.m_offset = patch_subnode.attribute("offset").as_uint(0); + memory.m_value = ReadHexString(patch_subnode.attribute("value").as_string()); + memory.m_valuefile = patch_subnode.attribute("valuefile").as_string(); + memory.m_original = ReadHexString(patch_subnode.attribute("original").as_string()); + memory.m_ocarina = patch_subnode.attribute("ocarina").as_bool(false); + memory.m_search = patch_subnode.attribute("search").as_bool(false); + memory.m_align = patch_subnode.attribute("align").as_uint(1); + } + } + } + + return disc; +} + +static bool CheckRegion(const std::vector& xml_regions, std::string_view game_region) +{ + if (xml_regions.begin() == xml_regions.end()) + return true; + + for (const auto& region : xml_regions) + { + if (region == game_region) + return true; + } + + return false; +} + +bool Disc::IsValidForGame(const std::string& game_id, std::optional revision, + std::optional disc_number) const +{ + if (game_id.size() != 6) + return false; + + const std::string_view game_id_full = std::string_view(game_id); + const std::string_view game_region = game_id_full.substr(3, 1); + const std::string_view game_developer = game_id_full.substr(4, 2); + const int disc_number_int = std::optional(disc_number).value_or(-1); + const int revision_int = std::optional(revision).value_or(-1); + + if (m_game_filter.m_game && !StringBeginsWith(game_id_full, *m_game_filter.m_game)) + return false; + if (m_game_filter.m_developer && game_developer != *m_game_filter.m_developer) + return false; + if (m_game_filter.m_disc && disc_number_int != *m_game_filter.m_disc) + return false; + if (m_game_filter.m_version && revision_int != *m_game_filter.m_version) + return false; + if (m_game_filter.m_regions && !CheckRegion(*m_game_filter.m_regions, game_region)) + return false; + + return true; +} + +std::vector Disc::GeneratePatches(const std::string& game_id) const +{ + const std::string_view game_id_full = std::string_view(game_id); + const std::string_view game_id_no_region = game_id_full.substr(0, 3); + const std::string_view game_region = game_id_full.substr(3, 1); + const std::string_view game_developer = game_id_full.substr(4, 2); + + const auto replace_variables = + [&](std::string_view sv, + const std::vector>& replacements) { + std::string result; + result.reserve(sv.size()); + while (!sv.empty()) + { + bool replaced = false; + for (const auto& r : replacements) + { + if (StringBeginsWith(sv, r.first)) + { + for (char c : r.second) + result.push_back(c); + sv = sv.substr(r.first.size()); + replaced = true; + break; + } + } + if (replaced) + continue; + result.push_back(sv[0]); + sv = sv.substr(1); + } + return result; + }; + + // Take only selected patches, replace placeholders in all strings, and return them. + std::vector active_patches; + for (const auto& section : m_sections) + { + for (const auto& option : section.m_options) + { + const u32 selected = option.m_selected_choice; + if (selected == 0 || selected > option.m_choices.size()) + continue; + const Choice& choice = option.m_choices[selected - 1]; + for (const auto& patch_ref : choice.m_patch_references) + { + const auto patch = std::find_if(m_patches.begin(), m_patches.end(), + [&](const Patch& p) { return patch_ref.m_id == p.m_id; }); + if (patch == m_patches.end()) + continue; + + std::vector> replacements; + replacements.emplace_back(std::pair{"{$__gameid}", game_id_no_region}); + replacements.emplace_back(std::pair{"{$__region}", game_region}); + replacements.emplace_back(std::pair{"{$__maker}", game_developer}); + for (const auto& param : patch_ref.m_params) + replacements.emplace_back(std::pair{"{$" + param.first + "}", param.second}); + + Patch& new_patch = active_patches.emplace_back(*patch); + new_patch.m_root = replace_variables(new_patch.m_root, replacements); + for (auto& file : new_patch.m_file_patches) + { + file.m_disc = replace_variables(file.m_disc, replacements); + file.m_external = replace_variables(file.m_external, replacements); + } + for (auto& folder : new_patch.m_folder_patches) + { + folder.m_disc = replace_variables(folder.m_disc, replacements); + folder.m_external = replace_variables(folder.m_external, replacements); + } + for (auto& savegame : new_patch.m_savegame_patches) + { + savegame.m_external = replace_variables(savegame.m_external, replacements); + } + for (auto& memory : new_patch.m_memory_patches) + { + memory.m_valuefile = replace_variables(memory.m_valuefile, replacements); + } + } + } + } + + return active_patches; +} + +std::vector GenerateRiivolutionPatchesFromGameModDescriptor( + const GameModDescriptorRiivolution& descriptor, const std::string& game_id, + std::optional revision, std::optional disc_number) +{ + std::vector result; + for (const auto& patch_info : descriptor.patches) + { + auto parsed = ParseFile(patch_info.xml); + if (!parsed || !parsed->IsValidForGame(game_id, revision, disc_number)) + continue; + + for (auto& section : parsed->m_sections) + { + for (auto& option : section.m_options) + { + const auto* info = [&]() -> const DiscIO::GameModDescriptorRiivolutionPatchOption* { + for (const auto& o : patch_info.options) + { + if (o.section_name == section.m_name) + { + if (!o.option_id.empty() && o.option_id == option.m_id) + return &o; + if (!o.option_name.empty() && o.option_name == option.m_name) + return &o; + } + } + return nullptr; + }(); + if (info && info->choice <= option.m_choices.size()) + option.m_selected_choice = info->choice; + } + } + + for (auto& p : parsed->GeneratePatches(game_id)) + { + p.m_file_data_loader = std::make_shared( + patch_info.root, parsed->m_xml_path, p.m_root); + result.emplace_back(std::move(p)); + } + } + return result; +} + +std::optional ParseConfigFile(const std::string& filename) +{ + ::File::IOFile f(filename, "rb"); + if (!f) + return std::nullopt; + + std::vector data; + data.resize(f.GetSize()); + if (!f.ReadBytes(data.data(), data.size())) + return std::nullopt; + + return ParseConfigString(std::string_view(data.data(), data.size())); +} + +std::optional ParseConfigString(std::string_view xml) +{ + pugi::xml_document doc; + const auto parse_result = doc.load_buffer(xml.data(), xml.size()); + if (!parse_result) + return std::nullopt; + + const auto riivolution = doc.child("riivolution"); + if (!riivolution) + return std::nullopt; + + Config config; + config.m_version = riivolution.attribute("version").as_int(-1); + if (config.m_version != 2) + return std::nullopt; + + const auto options = riivolution.children("option"); + for (const auto& option_node : options) + { + auto& option = config.m_options.emplace_back(); + option.m_id = option_node.attribute("id").as_string(); + option.m_default = option_node.attribute("default").as_uint(0); + } + + return config; +} + +std::string WriteConfigString(const Config& config) +{ + pugi::xml_document doc; + auto riivolution = doc.append_child("riivolution"); + riivolution.append_attribute("version").set_value(config.m_version); + for (const auto& option : config.m_options) + { + auto option_node = riivolution.append_child("option"); + option_node.append_attribute("id").set_value(option.m_id.c_str()); + option_node.append_attribute("default").set_value(option.m_default); + } + + std::stringstream ss; + doc.print(ss, " "); + return ss.str(); +} + +bool WriteConfigFile(const std::string& filename, const Config& config) +{ + auto xml = WriteConfigString(config); + if (xml.empty()) + return false; + + ::File::IOFile f(filename, "wb"); + if (!f) + return false; + + if (!f.WriteString(xml)) + return false; + + return true; +} + +void ApplyConfigDefaults(Disc* disc, const Config& config) +{ + for (const auto& config_option : config.m_options) + { + auto* matching_option = [&]() -> DiscIO::Riivolution::Option* { + for (auto& section : disc->m_sections) + { + for (auto& option : section.m_options) + { + if (option.m_id.empty()) + { + if ((section.m_name + option.m_name) == config_option.m_id) + return &option; + } + else + { + if (option.m_id == config_option.m_id) + return &option; + } + } + } + return nullptr; + }(); + if (matching_option) + matching_option->m_selected_choice = config_option.m_default; + } +} +} // namespace DiscIO::Riivolution diff --git a/Source/Core/DiscIO/RiivolutionParser.h b/Source/Core/DiscIO/RiivolutionParser.h new file mode 100644 index 0000000000..d826425e5a --- /dev/null +++ b/Source/Core/DiscIO/RiivolutionParser.h @@ -0,0 +1,231 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" + +namespace DiscIO +{ +struct GameModDescriptorRiivolution; +} + +namespace DiscIO::Riivolution +{ +class FileDataLoader; + +// Data to determine the game patches are valid for. +struct GameFilter +{ + std::optional m_game; + std::optional m_developer; + std::optional m_disc; + std::optional m_version; + std::optional> m_regions; +}; + +// Which patches will get activated by selecting a Choice in the Riivolution GUI. +struct PatchReference +{ + std::string m_id; + std::map m_params; +}; + +// A single choice within an Option in the Riivolution GUI. +struct Choice +{ + std::string m_name; + std::vector m_patch_references; +}; + +// A single option presented to the user in the Riivolution GUI. +struct Option +{ + std::string m_name; + std::string m_id; + std::vector m_choices; + + // The currently selected patch choice in the m_choices vector. + // Note that this index is 1-based; 0 means no choice is selected and this Option is disabled. + u32 m_selected_choice; +}; + +// A single page of options presented to the user in the Riivolution GUI. +struct Section +{ + std::string m_name; + std::vector