Merge pull request #10127 from AdmiralCurtiss/riivolution

HLE Riivolution patch support
This commit is contained in:
Léo Lam 2021-10-24 00:57:54 +02:00 committed by GitHub
commit 5d5f019921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2917 additions and 229 deletions

View File

@ -47,6 +47,7 @@
#define SCREENSHOTS_DIR "ScreenShots" #define SCREENSHOTS_DIR "ScreenShots"
#define LOAD_DIR "Load" #define LOAD_DIR "Load"
#define HIRES_TEXTURES_DIR "Textures" #define HIRES_TEXTURES_DIR "Textures"
#define RIIVOLUTION_DIR "Riivolution"
#define DUMP_DIR "Dump" #define DUMP_DIR "Dump"
#define DUMP_TEXTURES_DIR "Textures" #define DUMP_TEXTURES_DIR "Textures"
#define DUMP_FRAMES_DIR "Frames" #define DUMP_FRAMES_DIR "Frames"

View File

@ -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_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_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_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_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_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; 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: 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_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; s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP;
break; break;
} }

View File

@ -41,6 +41,7 @@ enum
D_STATESAVES_IDX, D_STATESAVES_IDX,
D_SCREENSHOTS_IDX, D_SCREENSHOTS_IDX,
D_HIRESTEXTURES_IDX, D_HIRESTEXTURES_IDX,
D_RIIVOLUTION_IDX,
D_DUMP_IDX, D_DUMP_IDX,
D_DUMPFRAMES_IDX, D_DUMPFRAMES_IDX,
D_DUMPOBJECTS_IDX, D_DUMPOBJECTS_IDX,

View File

@ -57,6 +57,9 @@ namespace fs = std::filesystem;
#include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/PowerPC.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "DiscIO/GameModDescriptor.h"
#include "DiscIO/RiivolutionParser.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeDisc.h"
#include "DiscIO/VolumeWad.h" #include "DiscIO/VolumeWad.h"
@ -216,6 +219,31 @@ BootParameters::GenerateFromFile(std::vector<std::string> paths,
return std::make_unique<BootParameters>(std::move(*wad), savestate_path); return std::make_unique<BootParameters>(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<Disc>(boot_params->parameters))
{
const auto& volume = *std::get<Disc>(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); PanicAlertFmtT("Could not recognize file {0}", path);
return {}; return {};
} }
@ -422,7 +450,10 @@ bool CBoot::BootUp(std::unique_ptr<BootParameters> boot)
struct BootTitle struct BootTitle
{ {
BootTitle() : config(SConfig::GetInstance()) {} BootTitle(const std::vector<DiscIO::Riivolution::Patch>& patches)
: config(SConfig::GetInstance()), riivolution_patches(patches)
{
}
bool operator()(BootParameters::Disc& disc) const bool operator()(BootParameters::Disc& disc) const
{ {
NOTICE_LOG_FMT(BOOT, "Booting from disc: {}", disc.path); NOTICE_LOG_FMT(BOOT, "Booting from disc: {}", disc.path);
@ -432,7 +463,7 @@ bool CBoot::BootUp(std::unique_ptr<BootParameters> boot)
if (!volume) if (!volume)
return false; return false;
if (!EmulatedBS2(config.bWii, *volume)) if (!EmulatedBS2(config.bWii, *volume, riivolution_patches))
return false; return false;
SConfig::OnNewTitleLoad(); SConfig::OnNewTitleLoad();
@ -542,11 +573,14 @@ bool CBoot::BootUp(std::unique_ptr<BootParameters> boot)
private: private:
const SConfig& config; const SConfig& config;
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches;
}; };
if (!std::visit(BootTitle(), boot->parameters)) if (!std::visit(BootTitle(boot->riivolution_patches), boot->parameters))
return false; return false;
DiscIO::Riivolution::ApplyGeneralMemoryPatches(boot->riivolution_patches);
return true; return true;
} }
@ -604,3 +638,20 @@ void CreateSystemMenuTitleDirs()
const auto es = IOS::HLE::GetIOS()->GetES(); const auto es = IOS::HLE::GetIOS()->GetES();
es->CreateTitleDirectories(Titles::SYSTEM_MENU, IOS::SYSMENU_GID); es->CreateTitleDirectories(Titles::SYSTEM_MENU, IOS::SYSMENU_GID);
} }
void AddRiivolutionPatches(BootParameters* boot_params,
std::vector<DiscIO::Riivolution::Patch> riivolution_patches)
{
if (riivolution_patches.empty())
return;
if (!std::holds_alternative<BootParameters::Disc>(boot_params->parameters))
return;
auto& disc = std::get<BootParameters::Disc>(boot_params->parameters);
disc.volume = DiscIO::CreateDisc(DiscIO::DirectoryBlobReader::Create(
std::move(disc.volume),
[&](std::vector<DiscIO::FSTBuilderNode>* fst, DiscIO::FSTBuilderNode* dol_node) {
DiscIO::Riivolution::ApplyPatchesToFiles(riivolution_patches, fst, dol_node);
}));
boot_params->riivolution_patches = std::move(riivolution_patches);
}

View File

@ -15,6 +15,7 @@
#include "Core/IOS/IOSC.h" #include "Core/IOS/IOSC.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "DiscIO/RiivolutionParser.h"
#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeDisc.h"
#include "DiscIO/VolumeWad.h" #include "DiscIO/VolumeWad.h"
@ -78,6 +79,7 @@ struct BootParameters
BootParameters(Parameters&& parameters_, const std::optional<std::string>& savestate_path_ = {}); BootParameters(Parameters&& parameters_, const std::optional<std::string>& savestate_path_ = {});
Parameters parameters; Parameters parameters;
std::vector<DiscIO::Riivolution::Patch> riivolution_patches;
std::optional<std::string> savestate_path; std::optional<std::string> savestate_path;
bool delete_savestate = false; bool delete_savestate = false;
}; };
@ -113,10 +115,14 @@ private:
static void SetupMSR(); static void SetupMSR();
static void SetupBAT(bool is_wii); static void SetupBAT(bool is_wii);
static bool RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume); static bool RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume,
static bool EmulatedBS2_GC(const DiscIO::VolumeDisc& volume); const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches);
static bool EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume); static bool EmulatedBS2_GC(const DiscIO::VolumeDisc& volume,
static bool EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume); const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches);
static bool EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume,
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches);
static bool EmulatedBS2(bool is_wii, const DiscIO::VolumeDisc& volume,
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches);
static bool Load_BS2(const std::string& boot_rom_filename); static bool Load_BS2(const std::string& boot_rom_filename);
static void SetupGCMemory(); static void SetupGCMemory();
@ -161,3 +167,6 @@ void UpdateStateFlags(std::function<void(StateFlags*)> update_function);
/// Normally, this is automatically done by ES when the System Menu is installed, /// 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. /// but we cannot rely on this because we don't require any system titles to be installed.
void CreateSystemMenuTitleDirs(); void CreateSystemMenuTitleDirs();
void AddRiivolutionPatches(BootParameters* boot_params,
std::vector<DiscIO::Riivolution::Patch> riivolution_patches);

View File

@ -33,6 +33,7 @@
#include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/PowerPC.h"
#include "DiscIO/Enums.h" #include "DiscIO/Enums.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeDisc.h"
namespace namespace
@ -87,7 +88,8 @@ void CBoot::SetupBAT(bool is_wii)
PowerPC::IBATUpdated(); 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<DiscIO::Riivolution::Patch>& riivolution_patches)
{ {
const DiscIO::Partition partition = volume.GetGamePartition(); const DiscIO::Partition partition = volume.GetGamePartition();
@ -148,6 +150,8 @@ bool CBoot::RunApploader(bool is_wii, const DiscIO::VolumeDisc& volume)
ram_address, length); ram_address, length);
DVDRead(volume, dvd_offset, ram_address, length, partition); DVDRead(volume, dvd_offset, ram_address, length, partition);
DiscIO::Riivolution::ApplyApploaderMemoryPatches(riivolution_patches, ram_address, length);
PowerPC::ppcState.gpr[3] = 0x81300004; PowerPC::ppcState.gpr[3] = 0x81300004;
PowerPC::ppcState.gpr[4] = 0x81300008; PowerPC::ppcState.gpr[4] = 0x81300008;
PowerPC::ppcState.gpr[5] = 0x8130000c; PowerPC::ppcState.gpr[5] = 0x8130000c;
@ -203,7 +207,8 @@ void CBoot::SetupGCMemory()
// GameCube Bootstrap 2 HLE: // GameCube Bootstrap 2 HLE:
// copy the apploader to 0x81200000 // copy the apploader to 0x81200000
// execute the apploader, function by function, using the above utility. // 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<DiscIO::Riivolution::Patch>& riivolution_patches)
{ {
INFO_LOG_FMT(BOOT, "Faking GC BS2..."); 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) // Global pointer to Small Data Area Base (Luigi's Mansion's apploader uses it)
PowerPC::ppcState.gpr[13] = ntsc ? 0x81465320 : 0x814b4fc0; 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) static DiscIO::Region CodeRegion(char c)
@ -436,7 +441,8 @@ static void WriteEmptyPlayRecord()
// Wii Bootstrap 2 HLE: // Wii Bootstrap 2 HLE:
// copy the apploader to 0x81200000 // copy the apploader to 0x81200000
// execute the apploader // execute the apploader
bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume) bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume,
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches)
{ {
INFO_LOG_FMT(BOOT, "Faking Wii BS2..."); INFO_LOG_FMT(BOOT, "Faking Wii BS2...");
if (volume.GetVolumeType() != DiscIO::Platform::WiiDisc) if (volume.GetVolumeType() != DiscIO::Platform::WiiDisc)
@ -493,7 +499,7 @@ bool CBoot::EmulatedBS2_Wii(const DiscIO::VolumeDisc& volume)
PowerPC::ppcState.gpr[1] = 0x816ffff0; // StackPointer PowerPC::ppcState.gpr[1] = 0x816ffff0; // StackPointer
if (!RunApploader(/*is_wii*/ true, volume)) if (!RunApploader(/*is_wii*/ true, volume, riivolution_patches))
return false; return false;
// The Apploader probably just overwrote values needed for RAM Override. Run this again! // 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 // 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. // 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<DiscIO::Riivolution::Patch>& riivolution_patches)
{ {
return is_wii ? EmulatedBS2_Wii(volume) : EmulatedBS2_GC(volume); return is_wii ? EmulatedBS2_Wii(volume, riivolution_patches) :
EmulatedBS2_GC(volume, riivolution_patches);
} }

View File

@ -418,6 +418,10 @@ bool BootCore(std::unique_ptr<BootParameters> boot, const WindowSystemInfo& wsi)
if (StartUp.bWii && DiscIO::IsNTSC(StartUp.m_region) && Config::Get(Config::SYSCONF_PAL60)) if (StartUp.bWii && DiscIO::IsNTSC(StartUp.m_region) && Config::Get(Config::SYSCONF_PAL60))
Config::SetCurrent(Config::SYSCONF_PAL60, false); 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); Core::UpdateWantDeterminism(/*initial*/ true);
if (StartUp.bWii) if (StartUp.bWii)

View File

@ -73,6 +73,8 @@
#include "Core/MemoryWatcher.h" #include "Core/MemoryWatcher.h"
#endif #endif
#include "DiscIO/RiivolutionPatcher.h"
#include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/GCAdapter.h" #include "InputCommon/GCAdapter.h"
@ -603,6 +605,10 @@ static void EmuThread(std::unique_ptr<BootParameters> boot, WindowSystemInfo wsi
else else
cpuThreadFunc = CpuThread; cpuThreadFunc = CpuThread;
std::optional<DiscIO::Riivolution::SavegameRedirect> savegame_redirect = std::nullopt;
if (SConfig::GetInstance().bWii)
savegame_redirect = DiscIO::Riivolution::ExtractSavegameRedirect(boot->riivolution_patches);
if (!CBoot::BootUp(std::move(boot))) if (!CBoot::BootUp(std::move(boot)))
return; return;
@ -611,7 +617,7 @@ static void EmuThread(std::unique_ptr<BootParameters> boot, WindowSystemInfo wsi
// with the correct title context since save copying requires title directories to exist. // with the correct title context since save copying requires title directories to exist.
Common::ScopeGuard wiifs_guard{&Core::CleanUpWiiFileSystemContents}; Common::ScopeGuard wiifs_guard{&Core::CleanUpWiiFileSystemContents};
if (SConfig::GetInstance().bWii) if (SConfig::GetInstance().bWii)
Core::InitializeWiiFileSystemContents(); Core::InitializeWiiFileSystemContents(savegame_redirect);
else else
wiifs_guard.Dismiss(); wiifs_guard.Dismiss();

View File

@ -72,6 +72,15 @@ enum class SeekMode : u32
using FileAttribute = u8; 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 struct Modes
{ {
Mode owner, group, other; Mode owner, group, other;
@ -239,6 +248,8 @@ public:
virtual Result<NandStats> GetNandStats() = 0; virtual Result<NandStats> GetNandStats() = 0;
/// Get usage information about a directory (used cluster and inode counts). /// Get usage information about a directory (used cluster and inode counts).
virtual Result<DirectoryStats> GetDirectoryStats(const std::string& path) = 0; virtual Result<DirectoryStats> GetDirectoryStats(const std::string& path) = 0;
virtual void SetNandRedirects(std::vector<NandRedirect> nand_redirects) = 0;
}; };
template <typename T> template <typename T>
@ -269,7 +280,8 @@ enum class Location
Session, Session,
}; };
std::unique_ptr<FileSystem> MakeFileSystem(Location location = Location::Session); std::unique_ptr<FileSystem> MakeFileSystem(Location location = Location::Session,
std::vector<NandRedirect> nand_redirects = {});
/// Convert a FS result code to an IOS error code. /// Convert a FS result code to an IOS error code.
IOS::HLE::ReturnCode ConvertResult(ResultCode code); IOS::HLE::ReturnCode ConvertResult(ResultCode code);

View File

@ -28,11 +28,12 @@ SplitPathResult SplitPathAndBasename(std::string_view path)
std::string(path.substr(last_separator + 1))}; std::string(path.substr(last_separator + 1))};
} }
std::unique_ptr<FileSystem> MakeFileSystem(Location location) std::unique_ptr<FileSystem> MakeFileSystem(Location location,
std::vector<NandRedirect> nand_redirects)
{ {
const std::string nand_root = const std::string nand_root =
File::GetUserPath(location == Location::Session ? D_SESSION_WIIROOT_IDX : D_WIIROOT_IDX); File::GetUserPath(location == Location::Session ? D_SESSION_WIIROOT_IDX : D_WIIROOT_IDX);
return std::make_unique<HostFileSystem>(nand_root); return std::make_unique<HostFileSystem>(nand_root, std::move(nand_redirects));
} }
IOS::HLE::ReturnCode ConvertResult(ResultCode code) IOS::HLE::ReturnCode ConvertResult(ResultCode code)

View File

@ -22,13 +22,24 @@
namespace IOS::HLE::FS 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) 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); ASSERT(false);
return m_root_path; return HostFilename{m_root_path, false};
} }
// Get total filesize of contents of a directory (recursive) // 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); 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<NandRedirect> nand_redirects)
: m_root_path{root_path}, m_nand_redirects(std::move(nand_redirects))
{ {
File::CreateFullPath(m_root_path + "/"); File::CreateFullPath(m_root_path + "/");
ResetFst(); ResetFst();
@ -197,11 +210,12 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
if (!IsValidNonRootPath(path)) if (!IsValidNonRootPath(path))
return nullptr; 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()) if (!host_file_info.Exists())
return nullptr; return nullptr;
FstEntry* entry = &m_root_entry; FstEntry* entry = host_file.is_redirect ? &m_redirect_fst : &m_root_entry;
std::string complete_path = ""; std::string complete_path = "";
for (const std::string& component : SplitString(std::string(path.substr(1)), '/')) 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. // Fall back to dummy data to avoid breaking existing filesystems.
// This code path is also reached when creating a new file or directory; // This code path is also reached when creating a new file or directory;
// proper metadata is filled in later. // 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 = &entry->children.emplace_back();
entry->name = component; entry->name = component;
entry->data.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite}; entry->data.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite};
@ -241,7 +256,7 @@ void HostFileSystem::DoState(PointerWrap& p)
handle.host_file.reset(); handle.host_file.reset();
// handle /tmp // handle /tmp
std::string Path = BuildFilename("/tmp"); std::string Path = BuildFilename("/tmp").host_path;
if (p.GetMode() == PointerWrap::MODE_READ) if (p.GetMode() == PointerWrap::MODE_READ)
{ {
File::DeleteDirRecursively(Path); File::DeleteDirRecursively(Path);
@ -336,7 +351,7 @@ void HostFileSystem::DoState(PointerWrap& p)
p.Do(handle.wii_path); p.Do(handle.wii_path);
p.Do(handle.file_offset); p.Do(handle.file_offset);
if (handle.opened) 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; return ResultCode::AccessDenied;
if (m_root_path.empty()) if (m_root_path.empty())
return ResultCode::AccessDenied; return ResultCode::AccessDenied;
const std::string root = BuildFilename("/"); const std::string root = BuildFilename("/").host_path;
if (!File::DeleteDirRecursively(root) || !File::CreateDir(root)) if (!File::DeleteDirRecursively(root) || !File::CreateDir(root))
return ResultCode::UnknownError; return ResultCode::UnknownError;
ResetFst(); ResetFst();
@ -366,7 +381,7 @@ ResultCode HostFileSystem::CreateFileOrDirectory(Uid uid, Gid gid, const std::st
return ResultCode::TooManyPathComponents; return ResultCode::TooManyPathComponents;
const auto split_path = SplitPathAndBasename(path); 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); FstEntry* parent = GetFstEntryForPath(split_path.parent);
if (!parent) if (!parent)
@ -428,7 +443,7 @@ ResultCode HostFileSystem::Delete(Uid uid, Gid gid, const std::string& path)
if (!IsValidNonRootPath(path)) if (!IsValidNonRootPath(path))
return ResultCode::Invalid; 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); const auto split_path = SplitPathAndBasename(path);
FstEntry* parent = GetFstEntryForPath(split_path.parent); 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; return ResultCode::InUse;
} }
const std::string host_old_path = BuildFilename(old_path); const auto host_old_info = BuildFilename(old_path);
const std::string host_new_path = BuildFilename(new_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 there is already something of the same type at the new path, delete it.
if (File::Exists(host_new_path)) 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)) if (!File::Rename(host_old_path, host_new_path))
{ {
ERROR_LOG_FMT(IOS_FS, "Rename {} to {} - failed", host_old_path, host_new_path); if (host_old_info.is_redirect || host_new_info.is_redirect)
return ResultCode::NotFound; {
// 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. // Finally, remove the child from the old parent and move it to the new parent.
@ -544,7 +580,7 @@ Result<std::vector<std::string>> HostFileSystem::ReadDirectory(Uid uid, Gid gid,
if (entry->data.is_file) if (entry->data.is_file)
return ResultCode::Invalid; 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); File::FSTEntry host_entry = File::ScanDirectoryTree(host_path, false);
for (File::FSTEntry& child : host_entry.children) for (File::FSTEntry& child : host_entry.children)
{ {
@ -612,7 +648,7 @@ Result<Metadata> HostFileSystem::GetMetadata(Uid uid, Gid gid, const std::string
return ResultCode::NotFound; return ResultCode::NotFound;
Metadata metadata = entry->data; Metadata metadata = entry->data;
metadata.size = File::GetSize(BuildFilename(path)); metadata.size = File::GetSize(BuildFilename(path).host_path);
return metadata; return metadata;
} }
@ -631,7 +667,7 @@ ResultCode HostFileSystem::SetMetadata(Uid caller_uid, const std::string& path,
if (caller_uid != 0 && uid != entry->data.uid) if (caller_uid != 0 && uid != entry->data.uid)
return ResultCode::AccessDenied; 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) if (entry->data.uid != uid && entry->data.is_file && !is_empty)
return ResultCode::FileNotEmpty; return ResultCode::FileNotEmpty;
@ -667,7 +703,7 @@ Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_
return ResultCode::Invalid; return ResultCode::Invalid;
DirectoryStats stats{}; DirectoryStats stats{};
std::string path(BuildFilename(wii_path)); std::string path(BuildFilename(wii_path).host_path);
if (File::IsDirectory(path)) if (File::IsDirectory(path))
{ {
File::FSTEntry parent_dir = File::ScanDirectoryTree(path, true); File::FSTEntry parent_dir = File::ScanDirectoryTree(path, true);
@ -685,4 +721,8 @@ Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_
return stats; return stats;
} }
void HostFileSystem::SetNandRedirects(std::vector<NandRedirect> nand_redirects)
{
m_nand_redirects = std::move(nand_redirects);
}
} // namespace IOS::HLE::FS } // namespace IOS::HLE::FS

View File

@ -22,7 +22,7 @@ namespace IOS::HLE::FS
class HostFileSystem final : public FileSystem class HostFileSystem final : public FileSystem
{ {
public: public:
HostFileSystem(const std::string& root_path); HostFileSystem(const std::string& root_path, std::vector<NandRedirect> nand_redirects = {});
~HostFileSystem(); ~HostFileSystem();
void DoState(PointerWrap& p) override; void DoState(PointerWrap& p) override;
@ -56,6 +56,8 @@ public:
Result<NandStats> GetNandStats() override; Result<NandStats> GetNandStats() override;
Result<DirectoryStats> GetDirectoryStats(const std::string& path) override; Result<DirectoryStats> GetDirectoryStats(const std::string& path) override;
void SetNandRedirects(std::vector<NandRedirect> nand_redirects) override;
private: private:
struct FstEntry struct FstEntry
{ {
@ -83,7 +85,12 @@ private:
Handle* GetHandleFromFd(Fd fd); Handle* GetHandleFromFd(Fd fd);
Fd ConvertHandleToFd(const Handle* handle) const; 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<File::IOFile> OpenHostFile(const std::string& host_path); std::shared_ptr<File::IOFile> OpenHostFile(const std::string& host_path);
ResultCode CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path, ResultCode CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path,
@ -112,6 +119,9 @@ private:
std::string m_root_path; std::string m_root_path;
std::map<std::string, std::weak_ptr<File::IOFile>> m_open_files; std::map<std::string, std::weak_ptr<File::IOFile>> m_open_files;
std::array<Handle, 16> m_handles{}; std::array<Handle, 16> m_handles{};
FstEntry m_redirect_fst{};
std::vector<NandRedirect> m_nand_redirects;
}; };
} // namespace IOS::HLE::FS } // namespace IOS::HLE::FS

View File

@ -80,7 +80,7 @@ Result<FileHandle> HostFileSystem::OpenFile(Uid, Gid, const std::string& path, M
if (!handle) if (!handle)
return ResultCode::NoFreeHandle; return ResultCode::NoFreeHandle;
const std::string host_path = BuildFilename(path); const std::string host_path = BuildFilename(path).host_path;
if (!File::IsFile(host_path)) if (!File::IsFile(host_path))
{ {
*handle = Handle{}; *handle = Handle{};

View File

@ -498,7 +498,7 @@ void Kernel::AddDevice(std::unique_ptr<Device> device)
void Kernel::AddCoreDevices() void Kernel::AddCoreDevices()
{ {
m_fs = FS::MakeFileSystem(); m_fs = FS::MakeFileSystem(IOS::HLE::FS::Location::Session, Core::GetActiveNandRedirects());
ASSERT(m_fs); ASSERT(m_fs);
std::lock_guard lock(m_device_map_mutex); std::lock_guard lock(m_device_map_mutex);

View File

@ -4,6 +4,7 @@
#include "Core/WiiRoot.h" #include "Core/WiiRoot.h"
#include <cinttypes> #include <cinttypes>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@ -34,6 +35,12 @@ namespace FS = IOS::HLE::FS;
static std::string s_temp_wii_root; static std::string s_temp_wii_root;
static bool s_wii_root_initialized = false; static bool s_wii_root_initialized = false;
static std::vector<IOS::HLE::FS::NandRedirect> s_nand_redirects;
const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects()
{
return s_nand_redirects;
}
static bool CopyBackupFile(const std::string& path_from, const std::string& path_to) 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)); File::SetUserPath(D_SESSION_WIIROOT_IDX, File::GetUserPath(D_WIIROOT_IDX));
} }
s_nand_redirects.clear();
s_wii_root_initialized = true; s_wii_root_initialized = true;
} }
@ -213,6 +221,7 @@ void ShutdownWiiRoot()
s_temp_wii_root.clear(); s_temp_wii_root.clear();
} }
s_nand_redirects.clear();
s_wii_root_initialized = false; s_wii_root_initialized = false;
} }
@ -288,7 +297,8 @@ static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_sou
return true; return true;
} }
void InitializeWiiFileSystemContents() void InitializeWiiFileSystemContents(
std::optional<DiscIO::Riivolution::SavegameRedirect> save_redirect)
{ {
const auto fs = IOS::HLE::GetIOS()->GetFS(); const auto fs = IOS::HLE::GetIOS()->GetFS();
@ -299,14 +309,31 @@ void InitializeWiiFileSystemContents()
if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, "")) if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND"); WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND");
if (!WiiRootIsTemporary()) if (WiiRootIsTemporary())
return; {
// 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. InitializeDeterministicWiiSaves(fs.get());
SysConf sysconf{fs}; }
sysconf.Save(); else if (save_redirect)
{
InitializeDeterministicWiiSaves(fs.get()); 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() void CleanUpWiiFileSystemContents()

View File

@ -3,6 +3,16 @@
#pragma once #pragma once
#include <optional>
#include <vector>
#include "DiscIO/RiivolutionPatcher.h"
namespace IOS::HLE::FS
{
struct NandRedirect;
}
namespace Core namespace Core
{ {
enum class RestoreReason enum class RestoreReason
@ -21,6 +31,9 @@ void BackupWiiSettings();
void RestoreWiiSettings(RestoreReason reason); void RestoreWiiSettings(RestoreReason reason);
// Initialize or clean up the filesystem contents. // Initialize or clean up the filesystem contents.
void InitializeWiiFileSystemContents(); void InitializeWiiFileSystemContents(
std::optional<DiscIO::Riivolution::SavegameRedirect> save_redirect);
void CleanUpWiiFileSystemContents(); void CleanUpWiiFileSystemContents();
const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects();
} // namespace Core } // namespace Core

View File

@ -23,11 +23,17 @@ add_library(discio
FileSystemGCWii.h FileSystemGCWii.h
Filesystem.cpp Filesystem.cpp
Filesystem.h Filesystem.h
GameModDescriptor.cpp
GameModDescriptor.h
LaggedFibonacciGenerator.cpp LaggedFibonacciGenerator.cpp
LaggedFibonacciGenerator.h LaggedFibonacciGenerator.h
MultithreadedCompressor.h MultithreadedCompressor.h
NANDImporter.cpp NANDImporter.cpp
NANDImporter.h NANDImporter.h
RiivolutionParser.cpp
RiivolutionParser.h
RiivolutionPatcher.cpp
RiivolutionPatcher.h
ScrubbedBlob.cpp ScrubbedBlob.cpp
ScrubbedBlob.h ScrubbedBlob.h
TGCBlob.cpp TGCBlob.cpp

View File

@ -28,6 +28,7 @@
#include "Core/IOS/ES/Formats.h" #include "Core/IOS/ES/Formats.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/DiscUtils.h" #include "DiscIO/DiscUtils.h"
#include "DiscIO/VolumeDisc.h"
#include "DiscIO/VolumeWii.h" #include "DiscIO/VolumeWii.h"
#include "DiscIO/WiiEncryptionCache.h" #include "DiscIO/WiiEncryptionCache.h"
@ -40,9 +41,7 @@ static size_t ReadFileToVector(const std::string& path, std::vector<u8>* vector)
static void PadToAddress(u64 start_address, u64* address, u64* length, u8** buffer); static void PadToAddress(u64 start_address, u64* address, u64* length, u8** buffer);
static void Write32(u32 data, u32 offset, std::vector<u8>* buffer); static void Write32(u32 data, u32 offset, std::vector<u8>* buffer);
static u32 ComputeNameSize(const File::FSTEntry& parent_entry);
static std::string ASCIIToUppercase(std::string str); static std::string ASCIIToUppercase(std::string str);
static void ConvertUTF8NamesToSHIFTJIS(File::FSTEntry* parent_entry);
enum class PartitionType : u32 enum class PartitionType : u32
{ {
@ -61,18 +60,8 @@ constexpr u8 ENTRY_SIZE = 0x0c;
constexpr u8 FILE_ENTRY = 0; constexpr u8 FILE_ENTRY = 0;
constexpr u8 DIRECTORY_ENTRY = 1; constexpr u8 DIRECTORY_ENTRY = 1;
DiscContent::DiscContent(u64 offset, u64 size, const std::string& path) DiscContent::DiscContent(u64 offset, u64 size, ContentSource source)
: m_offset(offset), m_size(size), m_content_source(path) : m_offset(offset), m_size(size), m_content_source(std::move(source))
{
}
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)
{ {
} }
@ -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); const u64 bytes_to_read = std::min(m_size - offset_in_content, *length);
if (std::holds_alternative<std::string>(m_content_source)) if (std::holds_alternative<ContentFile>(m_content_source))
{ {
File::IOFile file(std::get<std::string>(m_content_source), "rb"); const auto& content = std::get<ContentFile>(m_content_source);
if (!file.Seek(offset_in_content, SEEK_SET) || !file.ReadBytes(*buffer, bytes_to_read)) 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; return false;
}
} }
else if (std::holds_alternative<const u8*>(m_content_source)) else if (std::holds_alternative<const u8*>(m_content_source))
{ {
const u8* const content_pointer = std::get<const u8*>(m_content_source) + offset_in_content; const u8* const content_pointer = std::get<const u8*>(m_content_source) + offset_in_content;
std::copy(content_pointer, content_pointer + bytes_to_read, *buffer); std::copy(content_pointer, content_pointer + bytes_to_read, *buffer);
} }
else else if (std::holds_alternative<ContentPartition>(m_content_source))
{ {
DirectoryBlobReader* blob = std::get<DirectoryBlobReader*>(m_content_source); const auto& content = std::get<ContentPartition>(m_content_source);
DirectoryBlobReader* blob = content.m_reader;
const u64 decrypted_size = m_size * VolumeWii::BLOCK_DATA_SIZE / VolumeWii::BLOCK_TOTAL_SIZE; 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, if (!blob->EncryptPartitionData(content.m_offset + offset_in_content, bytes_to_read, *buffer,
decrypted_size)) content.m_partition_data_offset, decrypted_size))
{ {
return false; return false;
} }
} }
else if (std::holds_alternative<ContentVolume>(m_content_source))
{
const auto& source = std::get<ContentVolume>(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<ContentFixedByte>(m_content_source))
{
const ContentFixedByte& source = std::get<ContentFixedByte>(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; *length -= bytes_to_read;
*buffer += bytes_to_read; *buffer += bytes_to_read;
@ -137,35 +150,23 @@ bool DiscContent::Read(u64* offset, u64* length, u8** buffer) const
return true; 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) if (size != 0)
m_contents.emplace(offset, size, path); m_contents.emplace(offset, size, std::move(source));
}
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);
} }
u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, const std::string& path) u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, const std::string& path)
{ {
const u64 size = File::GetSize(path); const u64 size = File::GetSize(path);
Add(offset, size, path); Add(offset, size, ContentFile{path, 0});
return size; return size;
} }
u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path) u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path)
{ {
const u64 size = std::min(File::GetSize(path), max_size); const u64 size = std::min(File::GetSize(path), max_size);
Add(offset, size, path); Add(offset, size, ContentFile{path, 0});
return size; return size;
} }
@ -355,6 +356,18 @@ std::unique_ptr<DirectoryBlobReader> DirectoryBlobReader::Create(const std::stri
return std::unique_ptr<DirectoryBlobReader>(new DirectoryBlobReader(partition_root, true_root)); return std::unique_ptr<DirectoryBlobReader>(new DirectoryBlobReader(partition_root, true_root));
} }
std::unique_ptr<DirectoryBlobReader> DirectoryBlobReader::Create(
std::unique_ptr<DiscIO::VolumeDisc> volume,
const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
fst_callback)
{
if (!volume)
return nullptr;
return std::unique_ptr<DirectoryBlobReader>(
new DirectoryBlobReader(std::move(volume), fst_callback));
}
DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root, DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root,
const std::string& true_root) const std::string& true_root)
: m_encryption_cache(this) : m_encryption_cache(this)
@ -370,8 +383,8 @@ DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root,
} }
else else
{ {
SetNonpartitionDiscHeader(game_partition.GetHeader(), game_partition_root); SetNonpartitionDiscHeaderFromFile(game_partition.GetHeader(), game_partition_root);
SetWiiRegionData(game_partition_root); SetWiiRegionDataFromFile(game_partition_root);
std::vector<PartitionWithType> partitions; std::vector<PartitionWithType> partitions;
partitions.emplace_back(std::move(game_partition), PartitionType::Game); 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<DiscIO::VolumeDisc> volume,
const std::function<void(std::vector<FSTBuilderNode>* 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<u8> 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<u8> 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<PartitionWithType> 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<PartitionType>(*type));
}
}
SetPartitions(std::move(partitions));
}
}
bool DirectoryBlobReader::Read(u64 offset, u64 length, u8* buffer) bool DirectoryBlobReader::Read(u64 offset, u64 length, u8* buffer)
{ {
if (offset + length > m_data_size) if (offset + length > m_data_size)
@ -468,44 +537,70 @@ u64 DirectoryBlobReader::GetDataSize() const
return m_data_size; return m_data_size;
} }
void DirectoryBlobReader::SetNonpartitionDiscHeader(const std::vector<u8>& partition_header, void DirectoryBlobReader::SetNonpartitionDiscHeaderFromFile(const std::vector<u8>& partition_header,
const std::string& game_partition_root) const std::string& game_partition_root)
{ {
m_disc_header_nonpartition.resize(WII_NONPARTITION_DISCHEADER_SIZE); std::vector<u8> header_bin(WII_NONPARTITION_DISCHEADER_SIZE);
const size_t header_bin_bytes_read = 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<u8>& partition_header,
std::vector<u8> 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 // 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, if (header_bin_size < m_disc_header_nonpartition.size())
partition_header.data() + m_disc_header_nonpartition.size(), {
m_disc_header_nonpartition.data() + header_bin_bytes_read); 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 // 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; m_disc_header_nonpartition[0x60] = 0;
if (header_bin_bytes_read < 0x61) if (header_bin_size < 0x61)
m_disc_header_nonpartition[0x61] = 0; m_disc_header_nonpartition[0x61] = 0;
m_encrypted = std::all_of(m_disc_header_nonpartition.data() + 0x60, m_encrypted = std::all_of(m_disc_header_nonpartition.data() + 0x60,
m_disc_header_nonpartition.data() + 0x64, [](u8 x) { return x == 0; }); 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<u8> 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<u8>& wii_region_data,
const std::string& log_path)
{ {
m_wii_region_data.resize(0x10, 0x00); 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); Write32(INVALID_REGION, 0, &m_wii_region_data);
const std::string region_bin_path = game_partition_root + "disc/region.bin"; std::copy_n(wii_region_data.begin(),
const size_t bytes_read = ReadFileToVector(region_bin_path, &m_wii_region_data); std::min<size_t>(wii_region_data.size(), WII_REGION_DATA_SIZE),
if (bytes_read < 0x4) m_wii_region_data.begin());
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);
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<PartitionWithType>&& partitions) void DirectoryBlobReader::SetPartitions(std::vector<PartitionWithType>&& partitions)
@ -562,16 +657,17 @@ void DirectoryBlobReader::SetPartitions(std::vector<PartitionWithType>&& partiti
SetPartitionHeader(&partitions[i].partition, partition_address); SetPartitionHeader(&partitions[i].partition, partition_address);
const u64 data_size = partitions[i].partition.GetDataSize(); const u64 data_size = partitions[i].partition.GetDataSize();
m_partitions.emplace(partition_address + PARTITION_DATA_OFFSET, const u64 partition_data_offset = partition_address + PARTITION_DATA_OFFSET;
std::move(partitions[i].partition)); m_partitions.emplace(partition_data_offset, std::move(partitions[i].partition));
m_nonpartition_contents.Add(partition_address + PARTITION_DATA_OFFSET, data_size, this); m_nonpartition_contents.Add(partition_data_offset, data_size,
ContentPartition{this, 0, partition_data_offset});
const u64 unaligned_next_partition_address = VolumeWii::EncryptedPartitionOffsetToRawOffset( const u64 unaligned_next_partition_address = VolumeWii::EncryptedPartitionOffsetToRawOffset(
data_size, Partition(partition_address), PARTITION_DATA_OFFSET); data_size, Partition(partition_address), PARTITION_DATA_OFFSET);
partition_address = Common::AlignUp(unaligned_next_partition_address, 0x10000ull); partition_address = Common::AlignUp(unaligned_next_partition_address, 0x10000ull);
} }
m_data_size = partition_address; 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 // 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 TMD_OFFSET = 0x2c0;
constexpr u32 H3_OFFSET = 0x4000; constexpr u32 H3_OFFSET = 0x4000;
const std::optional<DiscIO::Partition>& wrapped_partition = partition->GetWrappedPartition();
const std::string& partition_root = partition->GetRootDirectory(); const std::string& partition_root = partition->GetRootDirectory();
const u64 ticket_size = m_nonpartition_contents.CheckSizeAndAdd( u64 ticket_size;
partition_address + WII_PARTITION_TICKET_ADDRESS, WII_PARTITION_TICKET_SIZE, if (wrapped_partition)
partition_root + "ticket.bin"); {
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( u64 tmd_size;
partition_address + TMD_OFFSET, IOS::ES::MAX_TMD_SIZE, partition_root + "tmd.bin"); 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 cert_offset = Common::AlignUp(TMD_OFFSET + tmd_size, 0x20ull);
const u64 max_cert_size = H3_OFFSET - cert_offset; 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, u64 cert_size;
partition_root + "h3.bin"); 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<u64> 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; constexpr u32 PARTITION_HEADER_SIZE = 0x1c;
const u64 data_size = Common::AlignUp(partition->GetDataSize(), 0x7c00) / 0x7c00 * 0x8000; const u64 data_size = Common::AlignUp(partition->GetDataSize(), 0x7c00) / 0x7c00 * 0x8000;
m_partition_headers.emplace_back(PARTITION_HEADER_SIZE); std::vector<u8>& partition_header = m_extra_data.emplace_back(PARTITION_HEADER_SIZE);
std::vector<u8>& partition_header = m_partition_headers.back();
Write32(static_cast<u32>(tmd_size), 0x0, &partition_header); Write32(static_cast<u32>(tmd_size), 0x0, &partition_header);
Write32(TMD_OFFSET >> 2, 0x4, &partition_header); Write32(TMD_OFFSET >> 2, 0x4, &partition_header);
Write32(static_cast<u32>(cert_size), 0x8, &partition_header); Write32(static_cast<u32>(cert_size), 0x8, &partition_header);
@ -611,7 +768,8 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition,
Write32(PARTITION_DATA_OFFSET >> 2, 0x14, &partition_header); Write32(PARTITION_DATA_OFFSET >> 2, 0x14, &partition_header);
Write32(static_cast<u32>(data_size >> 2), 0x18, &partition_header); Write32(static_cast<u32>(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<u8> ticket_buffer(ticket_size); std::vector<u8> ticket_buffer(ticket_size);
m_nonpartition_contents.Read(partition_address + WII_PARTITION_TICKET_ADDRESS, 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()); partition->SetKey(ticket.GetTitleKey());
} }
static void GenerateBuilderNodesFromFileSystem(const DiscIO::VolumeDisc& volume,
const DiscIO::Partition& partition,
std::vector<FSTBuilderNode>* nodes,
const FileInfo& parent_info)
{
for (const FileInfo& file_info : parent_info)
{
if (file_info.IsDirectory())
{
std::vector<FSTBuilderNode> 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<BuilderContentSource> 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, DirectoryBlobPartition::DirectoryBlobPartition(const std::string& root_directory,
std::optional<bool> is_wii) std::optional<bool> is_wii)
: m_root_directory(root_directory) : m_root_directory(root_directory)
{ {
SetDiscHeaderAndDiscType(is_wii); SetDiscHeaderFromFile(m_root_directory + "sys/boot.bin");
SetBI2(); SetDiscType(is_wii);
BuildFST(SetDOL(SetApploader())); 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<bool> is_wii) DirectoryBlobPartition::DirectoryBlobPartition(
DiscIO::VolumeDisc* volume, const DiscIO::Partition& partition, std::optional<bool> is_wii,
const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
fst_callback)
: m_wrapped_partition(partition)
{
std::vector<u8> 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<u8> bi2(BI2_SIZE);
if (!volume->Read(BI2_ADDRESS, BI2_SIZE, bi2.data(), partition))
bi2.clear();
SetBI2(std::move(bi2));
std::vector<u8> 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<BuilderContentSource> 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<FSTBuilderNode> 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); 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) if (ReadFileToVector(boot_bin_path, &m_disc_header) < 0x20)
ERROR_LOG_FMT(DISCIO, "{} doesn't exist or is too small", boot_bin_path); 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<u8> 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<bool> is_wii)
{
if (is_wii.has_value()) if (is_wii.has_value())
{ {
m_is_wii = *is_wii; m_is_wii = *is_wii;
@ -648,44 +898,64 @@ void DirectoryBlobPartition::SetDiscHeaderAndDiscType(std::optional<bool> is_wii
m_is_wii = Common::swap32(&m_disc_header[0x18]) == WII_DISC_MAGIC; 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; const bool is_gc = Common::swap32(&m_disc_header[0x1c]) == GAMECUBE_DISC_MAGIC;
if (m_is_wii == is_gc) 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; m_address_shift = m_is_wii ? 2 : 0;
} }
void DirectoryBlobPartition::SetBI2() void DirectoryBlobPartition::SetBI2FromFile(const std::string& bi2_path)
{ {
m_bi2.resize(BI2_SIZE); m_bi2.resize(BI2_SIZE);
if (!m_is_wii) if (!m_is_wii)
Write32(INVALID_REGION, 0x18, &m_bi2); 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); const size_t bytes_read = ReadFileToVector(bi2_path, &m_bi2);
if (!m_is_wii && bytes_read < 0x1C) if (!m_is_wii && bytes_read < 0x1C)
ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", bi2_path); 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<u8> 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<u8> apploader(file.GetSize());
file.ReadBytes(apploader.data(), apploader.size());
return SetApploader(std::move(apploader), path);
}
u64 DirectoryBlobPartition::SetApploader(std::vector<u8> apploader, const std::string& log_path)
{ {
bool success = false; bool success = false;
const std::string path = m_root_directory + "sys/apploader.img"; m_apploader = std::move(apploader);
File::IOFile file(path, "rb"); if (m_apploader.size() < 0x20)
m_apploader.resize(file.GetSize());
if (m_apploader.size() < 0x20 || !file.ReadBytes(m_apploader.data(), m_apploader.size()))
{ {
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 else
{ {
const size_t apploader_size = 0x20 + Common::swap32(*(u32*)&m_apploader[0x14]) + const size_t apploader_size = 0x20 + Common::swap32(*(u32*)&m_apploader[0x14]) +
Common::swap32(*(u32*)&m_apploader[0x18]); Common::swap32(*(u32*)&m_apploader[0x18]);
if (apploader_size != m_apploader.size()) 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 else
success = true; success = true;
} }
@ -697,15 +967,15 @@ u64 DirectoryBlobPartition::SetApploader()
Write32(static_cast<u32>(-1), 0x10, &m_apploader); Write32(static_cast<u32>(-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 DOL address, 32 byte aligned (plus 32 byte padding)
return Common::AlignUp(APPLOADER_ADDRESS + m_apploader.size() + 0x20, 0x20ull); 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<u32>(dol_address >> m_address_shift), 0x0420, &m_disc_header); Write32(static_cast<u32>(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); 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<u32>(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<FSTBuilderNode> ConvertFSTEntriesToBuilderNodes(const File::FSTEntry& parent)
{
std::vector<FSTBuilderNode> nodes;
nodes.reserve(parent.children.size());
for (const File::FSTEntry& entry : parent.children)
{
std::variant<std::vector<BuilderContentSource>, std::vector<FSTBuilderNode>> content;
if (entry.isDirectory)
{
content = ConvertFSTEntriesToBuilderNodes(entry);
}
else
{
content =
std::vector<BuilderContentSource>{{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<FSTBuilderNode>* fst)
{
for (FSTBuilderNode& entry : *fst)
{
if (entry.IsFolder())
ConvertUTF8NamesToSHIFTJIS(&entry.GetFolderContent());
entry.m_filename = UTF8ToSHIFTJIS(entry.m_filename);
}
}
static u32 ComputeNameSize(const std::vector<FSTBuilderNode>& files)
{
u32 name_size = 0;
for (const FSTBuilderNode& entry : files)
{
if (entry.IsFolder())
name_size += ComputeNameSize(entry.GetFolderContent());
name_size += static_cast<u32>(entry.m_filename.length() + 1);
}
return name_size;
}
static size_t RecalculateFolderSizes(std::vector<FSTBuilderNode>* 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<FSTBuilderNode> root_nodes, u64 fst_address)
{ {
m_fst_data.clear(); 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); // 1 extra for the root entry
u64 total_entries = rootEntry.size + 1; // The root entry itself isn't counted in rootEntry.size u64 total_entries = RecalculateFolderSizes(&root_nodes) + 1;
const u64 name_table_offset = total_entries * ENTRY_SIZE; const u64 name_table_offset = total_entries * ENTRY_SIZE;
m_fst_data.resize(name_table_offset + name_table_size); m_fst_data.resize(name_table_offset + name_table_size);
@ -737,7 +1083,7 @@ void DirectoryBlobPartition::BuildFST(u64 fst_address)
// write root entry // write root entry
WriteEntryData(&fst_offset, DIRECTORY_ENTRY, 0, 0, total_entries, m_address_shift); WriteEntryData(&fst_offset, DIRECTORY_ENTRY, 0, 0, total_entries, m_address_shift);
WriteDirectory(rootEntry, &fst_offset, &name_offset, &current_data_address, root_offset, WriteDirectory(&root_nodes, &fst_offset, &name_offset, &current_data_address, root_offset,
name_table_offset); name_table_offset);
// overflow check, compare the aligned name offset with the aligned name table size // 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), 0x0428, &m_disc_header);
Write32((u32)(m_fst_data.size() >> m_address_shift), 0x042c, &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; 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); *name_offset += (u32)(name.length() + 1);
} }
void DirectoryBlobPartition::WriteDirectory(const File::FSTEntry& parent_entry, u32* fst_offset, void DirectoryBlobPartition::WriteDirectory(std::vector<FSTBuilderNode>* parent_entries,
u32* name_offset, u64* data_offset, u32* fst_offset, u32* name_offset, u64* data_offset,
u32 parent_entry_index, u64 name_table_offset) u32 parent_entry_index, u64 name_table_offset)
{ {
std::vector<File::FSTEntry> sorted_entries = parent_entry.children; std::vector<FSTBuilderNode>& sorted_entries = *parent_entries;
// Sort for determinism // Sort for determinism
std::sort(sorted_entries.begin(), sorted_entries.end(), std::sort(sorted_entries.begin(), sorted_entries.end(),
[](const File::FSTEntry& one, const File::FSTEntry& two) { [](const FSTBuilderNode& one, const FSTBuilderNode& two) {
const std::string one_upper = ASCIIToUppercase(one.virtualName); const std::string one_upper = ASCIIToUppercase(one.m_filename);
const std::string two_upper = ASCIIToUppercase(two.virtualName); const std::string two_upper = ASCIIToUppercase(two.m_filename);
return one_upper == two_upper ? one.virtualName < two.virtualName : return one_upper == two_upper ? one.m_filename < two.m_filename :
one_upper < two_upper; 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; u32 entry_index = *fst_offset / ENTRY_SIZE;
WriteEntryData(fst_offset, DIRECTORY_ENTRY, *name_offset, parent_entry_index, WriteEntryData(fst_offset, DIRECTORY_ENTRY, *name_offset, parent_entry_index,
entry_index + entry.size + 1, 0); entry_index + entry.m_size + 1, 0);
WriteEntryName(name_offset, entry.virtualName, name_table_offset); 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 else
{ {
// put entry in FST // 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); 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 // 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 // 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<u8>* buffer)
(*buffer)[offset] = data & 0xff; (*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) static std::string ASCIIToUppercase(std::string str)
{ {
std::transform(str.begin(), str.end(), str.begin(), std::transform(str.begin(), str.end(), str.begin(),

View File

@ -5,6 +5,7 @@
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional> #include <optional>
@ -16,6 +17,7 @@
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
#include "Common/FileUtil.h" #include "Common/FileUtil.h"
#include "DiscIO/Blob.h" #include "DiscIO/Blob.h"
#include "DiscIO/Volume.h"
#include "DiscIO/WiiEncryptionCache.h" #include "DiscIO/WiiEncryptionCache.h"
namespace File namespace File
@ -29,22 +31,108 @@ namespace DiscIO
enum class PartitionType : u32; enum class PartitionType : u32;
class DirectoryBlobReader; class DirectoryBlobReader;
class VolumeDisc;
// Returns true if the path is inside a DirectoryBlob and doesn't represent the DirectoryBlob itself // Returns true if the path is inside a DirectoryBlob and doesn't represent the DirectoryBlob itself
bool ShouldHideFromGameList(const std::string& volume_path); 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<ContentFile, // File
const u8*, // Memory
ContentPartition, // Partition
ContentVolume, // Volume
ContentFixedByte // Fixed value padding
>;
struct BuilderContentSource
{
u64 m_offset;
u64 m_size;
ContentSource m_source;
};
struct FSTBuilderNode
{
std::string m_filename;
u64 m_size;
std::variant<std::vector<BuilderContentSource>, std::vector<FSTBuilderNode>> m_content;
void* m_user_data = nullptr;
bool IsFile() const
{
return std::holds_alternative<std::vector<BuilderContentSource>>(m_content);
}
std::vector<BuilderContentSource>& GetFileContent()
{
return std::get<std::vector<BuilderContentSource>>(m_content);
}
const std::vector<BuilderContentSource>& GetFileContent() const
{
return std::get<std::vector<BuilderContentSource>>(m_content);
}
bool IsFolder() const { return std::holds_alternative<std::vector<FSTBuilderNode>>(m_content); }
std::vector<FSTBuilderNode>& GetFolderContent()
{
return std::get<std::vector<FSTBuilderNode>>(m_content);
}
const std::vector<FSTBuilderNode>& GetFolderContent() const
{
return std::get<std::vector<FSTBuilderNode>>(m_content);
}
};
class DiscContent class DiscContent
{ {
public: public:
using ContentSource = DiscContent(u64 offset, u64 size, ContentSource source);
std::variant<std::string, // File
const u8*, // Memory
DirectoryBlobReader* // Partition (which one it is is determined by m_offset)
>;
DiscContent(u64 offset, u64 size, const std::string& path);
DiscContent(u64 offset, u64 size, const u8* data);
DiscContent(u64 offset, u64 size, DirectoryBlobReader* blob);
// Provided because it's convenient when searching for DiscContent in an std::set // Provided because it's convenient when searching for DiscContent in an std::set
explicit DiscContent(u64 offset); explicit DiscContent(u64 offset);
@ -62,8 +150,13 @@ public:
bool operator>=(const DiscContent& other) const { return !(*this < other); } bool operator>=(const DiscContent& other) const { return !(*this < other); }
private: private:
// Position of this content chunk within its parent DiscContentContainer.
u64 m_offset; u64 m_offset;
// Number of bytes this content chunk takes up.
u64 m_size = 0; u64 m_size = 0;
// Where and how to find the data for this content chunk.
ContentSource m_content_source; ContentSource m_content_source;
}; };
@ -71,13 +164,11 @@ class DiscContentContainer
{ {
public: public:
template <typename T> template <typename T>
void Add(u64 offset, const std::vector<T>& vector) void AddReference(u64 offset, const std::vector<T>& vector)
{ {
return Add(offset, vector.size() * sizeof(T), reinterpret_cast<const u8*>(vector.data())); return Add(offset, vector.size() * sizeof(T), reinterpret_cast<const u8*>(vector.data()));
} }
void Add(u64 offset, u64 size, const std::string& path); void Add(u64 offset, u64 size, ContentSource source);
void Add(u64 offset, u64 size, const u8* data);
void Add(u64 offset, u64 size, DirectoryBlobReader* blob);
u64 CheckSizeAndAdd(u64 offset, const std::string& path); u64 CheckSizeAndAdd(u64 offset, const std::string& path);
u64 CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path); u64 CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path);
@ -92,6 +183,10 @@ class DirectoryBlobPartition
public: public:
DirectoryBlobPartition() = default; DirectoryBlobPartition() = default;
DirectoryBlobPartition(const std::string& root_directory, std::optional<bool> is_wii); DirectoryBlobPartition(const std::string& root_directory, std::optional<bool> is_wii);
DirectoryBlobPartition(DiscIO::VolumeDisc* volume, const DiscIO::Partition& partition,
std::optional<bool> is_wii,
const std::function<void(std::vector<FSTBuilderNode>* fst_nodes,
FSTBuilderNode* dol_node)>& fst_callback);
// We do not allow copying, because it might mess up the pointers inside DiscContents // We do not allow copying, because it might mess up the pointers inside DiscContents
DirectoryBlobPartition(const DirectoryBlobPartition&) = delete; DirectoryBlobPartition(const DirectoryBlobPartition&) = delete;
@ -104,27 +199,38 @@ public:
const std::string& GetRootDirectory() const { return m_root_directory; } const std::string& GetRootDirectory() const { return m_root_directory; }
const std::vector<u8>& GetHeader() const { return m_disc_header; } const std::vector<u8>& GetHeader() const { return m_disc_header; }
const DiscContentContainer& GetContents() const { return m_contents; } const DiscContentContainer& GetContents() const { return m_contents; }
const std::optional<DiscIO::Partition>& GetWrappedPartition() const
{
return m_wrapped_partition;
}
const std::array<u8, VolumeWii::AES_KEY_SIZE>& GetKey() const { return m_key; } const std::array<u8, VolumeWii::AES_KEY_SIZE>& GetKey() const { return m_key; }
void SetKey(std::array<u8, VolumeWii::AES_KEY_SIZE> key) { m_key = key; } void SetKey(std::array<u8, VolumeWii::AES_KEY_SIZE> key) { m_key = key; }
private: private:
void SetDiscHeaderAndDiscType(std::optional<bool> is_wii); void SetDiscHeaderFromFile(const std::string& boot_bin_path);
void SetBI2(); void SetDiscHeader(std::vector<u8> boot_bin);
void SetDiscType(std::optional<bool> is_wii);
void SetBI2FromFile(const std::string& bi2_path);
void SetBI2(std::vector<u8> bi2);
// Returns DOL address // Returns DOL address
u64 SetApploader(); u64 SetApploaderFromFile(const std::string& path);
u64 SetApploader(std::vector<u8> apploader, const std::string& log_path);
// Returns FST address // 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<FSTBuilderNode> root_nodes, u64 fst_address);
// FST creation // FST creation
void WriteEntryData(u32* entry_offset, u8 type, u32 name_offset, u64 data_offset, u64 length, void WriteEntryData(u32* entry_offset, u8 type, u32 name_offset, u64 data_offset, u64 length,
u32 address_shift); u32 address_shift);
void WriteEntryName(u32* name_offset, const std::string& name, u64 name_table_offset); 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, void WriteDirectory(std::vector<FSTBuilderNode>* parent_entries, u32* fst_offset,
u64* data_offset, u32 parent_entry_index, u64 name_table_offset); u32* name_offset, u64* data_offset, u32 parent_entry_index,
u64 name_table_offset);
DiscContentContainer m_contents; DiscContentContainer m_contents;
std::vector<u8> m_disc_header; std::vector<u8> m_disc_header;
@ -140,6 +246,8 @@ private:
u32 m_address_shift = 0; u32 m_address_shift = 0;
u64 m_data_size = 0; u64 m_data_size = 0;
std::optional<DiscIO::Partition> m_wrapped_partition = std::nullopt;
}; };
class DirectoryBlobReader : public BlobReader class DirectoryBlobReader : public BlobReader
@ -148,6 +256,10 @@ class DirectoryBlobReader : public BlobReader
public: public:
static std::unique_ptr<DirectoryBlobReader> Create(const std::string& dol_path); static std::unique_ptr<DirectoryBlobReader> Create(const std::string& dol_path);
static std::unique_ptr<DirectoryBlobReader> Create(
std::unique_ptr<DiscIO::VolumeDisc> volume,
const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
fst_callback);
// We do not allow copying, because it might mess up the pointers inside DiscContents // We do not allow copying, because it might mess up the pointers inside DiscContents
DirectoryBlobReader(const DirectoryBlobReader&) = delete; DirectoryBlobReader(const DirectoryBlobReader&) = delete;
@ -183,15 +295,21 @@ private:
explicit DirectoryBlobReader(const std::string& game_partition_root, explicit DirectoryBlobReader(const std::string& game_partition_root,
const std::string& true_root); const std::string& true_root);
explicit DirectoryBlobReader(std::unique_ptr<DiscIO::VolumeDisc> volume,
const std::function<void(std::vector<FSTBuilderNode>* fst_nodes,
FSTBuilderNode* dol_node)>& fst_callback);
const DirectoryBlobPartition* GetPartition(u64 offset, u64 size, u64 partition_data_offset) const; const DirectoryBlobPartition* GetPartition(u64 offset, u64 size, u64 partition_data_offset) const;
bool EncryptPartitionData(u64 offset, u64 size, u8* buffer, u64 partition_data_offset, bool EncryptPartitionData(u64 offset, u64 size, u8* buffer, u64 partition_data_offset,
u64 partition_data_decrypted_size); u64 partition_data_decrypted_size);
void SetNonpartitionDiscHeaderFromFile(const std::vector<u8>& partition_header,
const std::string& game_partition_root);
void SetNonpartitionDiscHeader(const std::vector<u8>& partition_header, void SetNonpartitionDiscHeader(const std::vector<u8>& partition_header,
const std::string& game_partition_root); std::vector<u8> header_bin);
void SetWiiRegionData(const std::string& game_partition_root); void SetWiiRegionDataFromFile(const std::string& game_partition_root);
void SetWiiRegionData(const std::vector<u8>& wii_region_data, const std::string& log_path);
void SetPartitions(std::vector<PartitionWithType>&& partitions); void SetPartitions(std::vector<PartitionWithType>&& partitions);
void SetPartitionHeader(DirectoryBlobPartition* partition, u64 partition_address); void SetPartitionHeader(DirectoryBlobPartition* partition, u64 partition_address);
@ -209,9 +327,11 @@ private:
std::vector<u8> m_disc_header_nonpartition; std::vector<u8> m_disc_header_nonpartition;
std::vector<u8> m_partition_table; std::vector<u8> m_partition_table;
std::vector<u8> m_wii_region_data; std::vector<u8> m_wii_region_data;
std::vector<std::vector<u8>> m_partition_headers; std::vector<std::vector<u8>> m_extra_data;
u64 m_data_size; u64 m_data_size;
std::unique_ptr<DiscIO::VolumeDisc> m_wrapped_volume;
}; };
} // namespace DiscIO } // namespace DiscIO

View File

@ -0,0 +1,147 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "DiscIO/GameModDescriptor.h"
#include <picojson.h>
#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<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename)
{
::File::IOFile f(filename, "rb");
if (!f)
return std::nullopt;
std::vector<char> 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<GameModDescriptorRiivolutionPatchOption>
ParseRiivolutionOptions(const picojson::array& array)
{
std::vector<GameModDescriptorRiivolutionPatchOption> options;
for (const auto& option_object : array)
{
if (!option_object.is<picojson::object>())
continue;
auto& option = options.emplace_back();
for (const auto& [key, value] : option_object.get<picojson::object>())
{
if (key == "section-name" && value.is<std::string>())
option.section_name = value.get<std::string>();
else if (key == "option-id" && value.is<std::string>())
option.option_id = value.get<std::string>();
else if (key == "option-name" && value.is<std::string>())
option.option_name = value.get<std::string>();
else if (key == "choice" && value.is<double>())
option.choice = MathUtil::SaturatingCast<u32>(value.get<double>());
}
}
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<picojson::array>())
{
for (const auto& patch_object : element_value.get<picojson::array>())
{
if (!patch_object.is<picojson::object>())
continue;
auto& patch = r.patches.emplace_back();
for (const auto& [key, value] : patch_object.get<picojson::object>())
{
if (key == "xml" && value.is<std::string>())
patch.xml = MakeAbsolute(json_directory, value.get<std::string>());
else if (key == "root" && value.is<std::string>())
patch.root = MakeAbsolute(json_directory, value.get<std::string>());
else if (key == "options" && value.is<picojson::array>())
patch.options = ParseRiivolutionOptions(value.get<picojson::array>());
}
}
}
}
return r;
}
std::optional<GameModDescriptor> 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<picojson::object>())
return std::nullopt;
GameModDescriptor descriptor;
bool is_valid_version = false;
for (const auto& [key, value] : json_root.get<picojson::object>())
{
if (key == "version" && value.is<double>())
{
is_valid_version = value.get<double>() == 1.0;
}
else if (key == "base-file" && value.is<std::string>())
{
descriptor.base_file = MakeAbsolute(json_directory, value.get<std::string>());
}
else if (key == "display-name" && value.is<std::string>())
{
descriptor.display_name = value.get<std::string>();
}
else if (key == "banner" && value.is<std::string>())
{
descriptor.banner = MakeAbsolute(json_directory, value.get<std::string>());
}
else if (key == "riivolution" && value.is<picojson::object>())
{
descriptor.riivolution =
ParseRiivolutionObject(json_directory, value.get<picojson::object>());
}
}
if (!is_valid_version)
return std::nullopt;
return descriptor;
}
} // namespace DiscIO

View File

@ -0,0 +1,46 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#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<GameModDescriptorRiivolutionPatchOption> options;
};
struct GameModDescriptorRiivolution
{
std::vector<GameModDescriptorRiivolutionPatch> patches;
};
struct GameModDescriptor
{
std::string base_file;
std::string display_name;
std::string banner;
std::optional<GameModDescriptorRiivolution> riivolution = std::nullopt;
};
std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename);
std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json,
std::string_view json_path);
} // namespace DiscIO

View File

@ -0,0 +1,486 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DiscIO/RiivolutionParser.h"
#include <algorithm>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include <pugixml.hpp>
#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<Disc> ParseFile(const std::string& filename)
{
::File::IOFile f(filename, "rb");
if (!f)
return std::nullopt;
std::vector<char> 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<std::string, std::string> ReadParams(const pugi::xml_node& node,
std::map<std::string, std::string> 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<u8> ReadHexString(std::string_view sv)
{
if ((sv.size() % 2) == 1)
return {};
if (StringBeginsWith(sv, "0x") || StringBeginsWith(sv, "0X"))
sv = sv.substr(2);
std::vector<u8> 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<Disc> 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<std::string> 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<std::string>& 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<u16> revision,
std::optional<u8> 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<int>(disc_number).value_or(-1);
const int revision_int = std::optional<int>(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<Patch> 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<std::pair<std::string, std::string_view>>& 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<Patch> 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<std::pair<std::string, std::string_view>> 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<Patch> GenerateRiivolutionPatchesFromGameModDescriptor(
const GameModDescriptorRiivolution& descriptor, const std::string& game_id,
std::optional<u16> revision, std::optional<u8> disc_number)
{
std::vector<Patch> 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<DiscIO::Riivolution::FileDataLoaderHostFS>(
patch_info.root, parsed->m_xml_path, p.m_root);
result.emplace_back(std::move(p));
}
}
return result;
}
std::optional<Config> ParseConfigFile(const std::string& filename)
{
::File::IOFile f(filename, "rb");
if (!f)
return std::nullopt;
std::vector<char> 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<Config> 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

View File

@ -0,0 +1,231 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#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<std::string> m_game;
std::optional<std::string> m_developer;
std::optional<int> m_disc;
std::optional<int> m_version;
std::optional<std::vector<std::string>> m_regions;
};
// Which patches will get activated by selecting a Choice in the Riivolution GUI.
struct PatchReference
{
std::string m_id;
std::map<std::string, std::string> m_params;
};
// A single choice within an Option in the Riivolution GUI.
struct Choice
{
std::string m_name;
std::vector<PatchReference> 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<Choice> 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<Option> m_options;
};
// Replaces, adds, or modifies a file on disc.
struct File
{
// Path of the file on disc to modify.
std::string m_disc;
// Path of the file on SD card to use for modification.
std::string m_external;
// If true, the file on the disc is truncated if the external file end is before the disc file
// end. If false, the bytes after the external file end stay as they were.
bool m_resize = true;
// If true, a new file is created if it does not already exist at the disc path. Otherwise this
// modification is ignored if the file does not exist on disc.
bool m_create = false;
// Offset of where to start replacing bytes in the on-disc file.
u32 m_offset = 0;
// Offset of where to start reading bytes in the external file.
u32 m_fileoffset = 0;
// Amount of bytes to copy from the external file to the disc file.
// If left zero, the entire file (starting at fileoffset) is used.
u32 m_length = 0;
};
// Adds or modifies a folder on disc.
struct Folder
{
// Path of the folder on disc to modify.
// Can be left empty to replace files matching the filename without specifying the folder.
std::string m_disc;
// Path of the folder on SD card to use for modification.
std::string m_external;
// Like File::m_resize but for each file in the folder.
bool m_resize = true;
// Like File::m_create but for each file in the folder.
bool m_create = false;
// Whether to also traverse subdirectories. (TODO: of the disc folder? external folder? both?)
bool m_recursive = true;
// Like File::m_length but for each file in the folder.
u32 m_length = 0;
};
// Redirects the save file from the Wii NAND to a folder on SD card.
struct Savegame
{
// The folder on SD card to use for the save files. Is created if it does not exist.
std::string m_external;
// If this is set to true and the external folder is empty or does not exist, the existing save on
// NAND is copied to the new folder on game boot.
bool m_clone = true;
};
// Modifies the game RAM right before jumping into the game executable.
struct Memory
{
// Memory address where this modification takes place.
u32 m_offset = 0;
// Bytes to write to that address.
std::vector<u8> m_value;
// Like m_value, but read the bytes from a file instead.
std::string m_valuefile;
// If set, the memory at that address will be checked before the value is written, and the
// replacement value only written if the bytes there match this.
std::vector<u8> m_original;
// If true, this memory patch is an ocarina-style patch.
// TODO: I'm unsure what this means exactly, need to check some examples...
bool m_ocarina = false;
// If true, the offset is not known, and instead we should search for the m_original bytes in
// memory and replace them where found. Only searches in MEM1, and only replaces the first match.
bool m_search = false;
// For m_search. The byte stride between search points.
u32 m_align = 1;
};
struct Patch
{
// Internal name of this patch.
std::string m_id;
// Defines a SD card path that all other paths are relative to.
// For actually loading file data Dolphin uses the loader below instead.
std::string m_root;
std::shared_ptr<FileDataLoader> m_file_data_loader;
std::vector<File> m_file_patches;
std::vector<Folder> m_folder_patches;
std::vector<Savegame> m_savegame_patches;
std::vector<Memory> m_memory_patches;
~Patch();
};
struct Disc
{
// Riivolution version. Only '1' exists at time of writing.
int m_version;
// Info about which game and revision these patches are for.
GameFilter m_game_filter;
// The options shown to the user in the UI.
std::vector<Section> m_sections;
// The actual patch instructions.
std::vector<Patch> m_patches;
// The path to the parsed XML file.
std::string m_xml_path;
// Checks whether these patches are valid for the given game.
bool IsValidForGame(const std::string& game_id, std::optional<u16> revision,
std::optional<u8> disc_number) const;
// Transforms an abstract XML-parsed patch set into a concrete one, with only the selected
// patches applied and all placeholders replaced.
std::vector<Patch> GeneratePatches(const std::string& game_id) const;
};
// Config format that remembers which patches are enabled/disabled for the next run.
// Some patches ship with pre-made config XMLs instead of baking their defaults into the actual
// patch XMLs, so it makes sense to support this format directly.
struct ConfigOption
{
// The identifier for the referenced Option.
std::string m_id;
// The selected Choice index.
u32 m_default;
};
struct Config
{
// Config version. Riivolution itself only accepts '2' here.
int m_version = 2;
std::vector<ConfigOption> m_options;
};
std::optional<Disc> ParseFile(const std::string& filename);
std::optional<Disc> ParseString(std::string_view xml, std::string xml_path);
std::vector<Patch> GenerateRiivolutionPatchesFromGameModDescriptor(
const GameModDescriptorRiivolution& descriptor, const std::string& game_id,
std::optional<u16> revision, std::optional<u8> disc_number);
std::optional<Config> ParseConfigFile(const std::string& filename);
std::optional<Config> ParseConfigString(std::string_view xml);
std::string WriteConfigString(const Config& config);
bool WriteConfigFile(const std::string& filename, const Config& config);
void ApplyConfigDefaults(Disc* disc, const Config& config);
} // namespace DiscIO::Riivolution

View File

@ -0,0 +1,654 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DiscIO/RiivolutionPatcher.h"
#include <algorithm>
#include <cctype>
#include <locale>
#include <string>
#include <string_view>
#include <vector>
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/StringUtil.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/PowerPC/MMU.h"
#include "DiscIO/DirectoryBlob.h"
#include "DiscIO/RiivolutionParser.h"
namespace DiscIO::Riivolution
{
FileDataLoader::~FileDataLoader() = default;
FileDataLoaderHostFS::FileDataLoaderHostFS(std::string sd_root, const std::string& xml_path,
std::string_view patch_root)
: m_sd_root(std::move(sd_root))
{
// Riivolution treats 'external' file paths as follows:
// - If it starts with a '/', it's an absolute path, ie. relative to the SD card root.
// - Otherwise:
// - If the 'root' parameter of the current patch is not set or is empty, the path is relative
// to the folder the XML file is in.
// - If the 'root' parameter of the current patch starts with a '/', the path is relative to
// that folder on the SD card, starting at the SD card root.
// - If the 'root' parameter of the current patch starts without a '/', the path is relative to
// that folder on the SD card, starting at the folder the XML file is in.
// The following initialization should properly replicate this behavior.
// First set m_patch_root to the folder the parsed XML file is in.
SplitPath(xml_path, &m_patch_root, nullptr, nullptr);
// Then try to resolve the given patch_root as if it was a file path, and on success replace the
// m_patch_root with it.
if (!patch_root.empty())
{
auto r = MakeAbsoluteFromRelative(patch_root);
if (r)
m_patch_root = std::move(*r);
}
}
std::optional<std::string>
FileDataLoaderHostFS::MakeAbsoluteFromRelative(std::string_view external_relative_path)
{
#ifdef _WIN32
// Riivolution treats a backslash as just a standard filename character, but we can't replicate
// this properly on Windows. So if a file contains a backslash, immediately error out.
if (external_relative_path.find("\\") != std::string_view::npos)
return std::nullopt;
#endif
std::string result = StringBeginsWith(external_relative_path, "/") ? m_sd_root : m_patch_root;
std::string_view work = external_relative_path;
// Strip away all leading and trailing path separators.
while (StringBeginsWith(work, "/"))
work.remove_prefix(1);
while (StringEndsWith(work, "/"))
work.remove_suffix(1);
size_t depth = 0;
while (true)
{
if (work.empty())
break;
// Extract a single path element.
size_t separator_position = work.find('/');
std::string_view element = work.substr(0, separator_position);
if (element == ".")
{
// This is a harmless element, doesn't change any state.
}
else if (element == "..")
{
// We're going up a level.
// If this isn't possible someone is trying to exit the root directory, prevent that.
if (depth == 0)
return std::nullopt;
--depth;
// Remove the last path element from the result string.
// This must have been previously attached in the branch below (otherwise depth would have
// been 0), so there's no need to check whether the string is empty or anything like that.
while (result.back() != '/')
result.pop_back();
result.pop_back();
}
else if (std::all_of(element.begin(), element.end(), [](char c) { return c == '.'; }))
{
// This is a triple, quadruple, etc. dot.
// Some file systems treat this as several 'up' path traversals, but Riivolution does not.
// If someone tries this just error out, it wouldn't work sensibly in Riivolution anyway.
return std::nullopt;
}
else
{
// We're going down a level.
++depth;
// Append path element to result string.
result += '/';
result += element;
}
// If this was the last path element, we're done.
if (separator_position == std::string_view::npos)
break;
// Remove element from work string.
work = work.substr(separator_position + 1);
// Remove any potential extra path separators.
while (StringBeginsWith(work, "/"))
work = work.substr(1);
}
return result;
}
std::optional<u64>
FileDataLoaderHostFS::GetExternalFileSize(std::string_view external_relative_path)
{
auto path = MakeAbsoluteFromRelative(external_relative_path);
if (!path)
return std::nullopt;
::File::FileInfo f(*path);
if (!f.IsFile())
return std::nullopt;
return f.GetSize();
}
std::vector<u8> FileDataLoaderHostFS::GetFileContents(std::string_view external_relative_path)
{
auto path = MakeAbsoluteFromRelative(external_relative_path);
if (!path)
return {};
::File::IOFile f(*path, "rb");
if (!f)
return {};
const u64 length = f.GetSize();
std::vector<u8> value;
value.resize(length);
if (!f.ReadBytes(value.data(), length))
return {};
return value;
}
std::vector<FileDataLoader::Node>
FileDataLoaderHostFS::GetFolderContents(std::string_view external_relative_path)
{
auto path = MakeAbsoluteFromRelative(external_relative_path);
if (!path)
return {};
::File::FSTEntry external_files = ::File::ScanDirectoryTree(*path, false);
std::vector<FileDataLoader::Node> nodes;
nodes.reserve(external_files.children.size());
for (auto& file : external_files.children)
nodes.emplace_back(FileDataLoader::Node{std::move(file.virtualName), file.isDirectory});
return nodes;
}
BuilderContentSource
FileDataLoaderHostFS::MakeContentSource(std::string_view external_relative_path,
u64 external_offset, u64 external_size, u64 disc_offset)
{
auto path = MakeAbsoluteFromRelative(external_relative_path);
if (!path)
return BuilderContentSource{disc_offset, external_size, ContentFixedByte{0}};
return BuilderContentSource{disc_offset, external_size,
ContentFile{std::move(*path), external_offset}};
}
std::optional<std::string>
FileDataLoaderHostFS::ResolveSavegameRedirectPath(std::string_view external_relative_path)
{
return MakeAbsoluteFromRelative(external_relative_path);
}
// 'before' and 'after' should be two copies of the same source
// 'split_at' needs to be between the start and end of the source, may not match either boundary
static void SplitAt(BuilderContentSource* before, BuilderContentSource* after, u64 split_at)
{
const u64 start = before->m_offset;
const u64 size = before->m_size;
const u64 end = start + size;
// The source before the split point just needs its length reduced.
before->m_size = split_at - start;
// The source after the split needs its length reduced and its start point adjusted.
after->m_offset += before->m_size;
after->m_size = end - split_at;
if (std::holds_alternative<ContentFile>(after->m_source))
std::get<ContentFile>(after->m_source).m_offset += before->m_size;
else if (std::holds_alternative<const u8*>(after->m_source))
std::get<const u8*>(after->m_source) += before->m_size;
else if (std::holds_alternative<ContentPartition>(after->m_source))
std::get<ContentPartition>(after->m_source).m_offset += before->m_size;
else if (std::holds_alternative<ContentVolume>(after->m_source))
std::get<ContentVolume>(after->m_source).m_offset += before->m_size;
}
static void ApplyPatchToFile(const Patch& patch, DiscIO::FSTBuilderNode* file_node,
std::string_view external_filename, u64 file_patch_offset,
u64 raw_external_file_offset, u64 file_patch_length, bool resize)
{
const auto f = patch.m_file_data_loader->GetExternalFileSize(external_filename);
if (!f)
return;
auto& content = std::get<std::vector<BuilderContentSource>>(file_node->m_content);
const u64 raw_external_filesize = *f;
const u64 external_file_offset = std::min(raw_external_file_offset, raw_external_filesize);
const u64 external_filesize = raw_external_filesize - external_file_offset;
const u64 patch_start = file_patch_offset;
const u64 patch_size = file_patch_length == 0 ? external_filesize : file_patch_length;
const u64 patch_end = patch_start + patch_size;
const u64 target_filesize = resize ? patch_end : std::max(file_node->m_size, patch_end);
size_t insert_where = 0;
if (patch_start >= file_node->m_size)
{
// If the patch is at or past the end of the existing file no existing content needs to be
// touched, just extend the file.
if (patch_start > file_node->m_size)
{
// Insert an padding area between the old file and the patch data.
content.emplace_back(BuilderContentSource{file_node->m_size, patch_start - file_node->m_size,
ContentFixedByte{0}});
}
insert_where = content.size();
}
else
{
// Patch is at the start or somewhere in the middle of the existing file. At least one source
// needs to be modified or removed, and a new source with the patch data inserted instead.
// To make this easier, we first split up existing sources at the patch start and patch end
// offsets, then discard all overlapping sources and insert the patch sources there.
for (size_t i = 0; i < content.size(); ++i)
{
const u64 source_start = content[i].m_offset;
const u64 source_end = source_start + content[i].m_size;
if (patch_start > source_start && patch_start < source_end)
{
content.insert(content.begin() + i + 1, content[i]);
SplitAt(&content[i], &content[i + 1], patch_start);
continue;
}
if (patch_end > source_start && patch_end < source_end)
{
content.insert(content.begin() + i + 1, content[i]);
SplitAt(&content[i], &content[i + 1], patch_end);
}
}
// Now discard the overlapping areas and remember where they were so we can insert there.
for (size_t i = 0; i < content.size(); ++i)
{
if (patch_start == content[i].m_offset)
{
insert_where = i;
while (i < content.size() && patch_end >= content[i].m_offset + content[i].m_size)
++i;
content.erase(content.begin() + insert_where, content.begin() + i);
break;
}
}
}
// Insert the actual patch data.
if (patch_size > 0 && external_filesize > 0)
{
BuilderContentSource source = patch.m_file_data_loader->MakeContentSource(
external_filename, external_file_offset, std::min(patch_size, external_filesize),
patch_start);
content.emplace(content.begin() + insert_where, std::move(source));
++insert_where;
}
// Pad with zeroes if the patch file is smaller than the patch size.
if (external_filesize < patch_size)
{
BuilderContentSource padding{patch_start + external_filesize, patch_size - external_filesize,
ContentFixedByte{0}};
content.emplace(content.begin() + insert_where, std::move(padding));
}
// Update the filesize of the file.
file_node->m_size = target_filesize;
// Drop any source past the new end of the file -- this can happen on file truncation.
while (!content.empty() && content.back().m_offset >= target_filesize)
content.pop_back();
}
static void ApplyPatchToFile(const Patch& patch, const File& file_patch,
DiscIO::FSTBuilderNode* file_node)
{
// The last two bits of the offset seem to be ignored by actual Riivolution.
ApplyPatchToFile(patch, file_node, file_patch.m_external, file_patch.m_offset & ~u64(3),
file_patch.m_fileoffset, file_patch.m_length, file_patch.m_resize);
}
static bool CaseInsensitiveEquals(std::string_view a, std::string_view b)
{
if (a.size() != b.size())
return false;
return std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) {
return std::tolower(ca, std::locale::classic()) == std::tolower(cb, std::locale::classic());
});
}
static FSTBuilderNode* FindFileNodeInFST(std::string_view path, std::vector<FSTBuilderNode>* fst,
bool create_if_not_exists)
{
const size_t path_separator = path.find('/');
const bool is_file = path_separator == std::string_view::npos;
const std::string_view name = is_file ? path : path.substr(0, path_separator);
const auto it = std::find_if(fst->begin(), fst->end(), [&](const FSTBuilderNode& node) {
return CaseInsensitiveEquals(node.m_filename, name);
});
if (it == fst->end())
{
if (!create_if_not_exists)
return nullptr;
if (is_file)
{
return &fst->emplace_back(
DiscIO::FSTBuilderNode{std::string(name), 0, std::vector<BuilderContentSource>()});
}
auto& new_folder = fst->emplace_back(
DiscIO::FSTBuilderNode{std::string(name), 0, std::vector<FSTBuilderNode>()});
return FindFileNodeInFST(path.substr(path_separator + 1),
&std::get<std::vector<FSTBuilderNode>>(new_folder.m_content), true);
}
const bool is_existing_node_file = it->IsFile();
if (is_file != is_existing_node_file)
return nullptr;
if (is_file)
return &*it;
return FindFileNodeInFST(path.substr(path_separator + 1),
&std::get<std::vector<FSTBuilderNode>>(it->m_content),
create_if_not_exists);
}
static void FindFilenameNodesInFST(std::vector<DiscIO::FSTBuilderNode*>* nodes_out,
std::string_view filename, std::vector<FSTBuilderNode>* fst)
{
for (FSTBuilderNode& node : *fst)
{
if (node.IsFolder())
{
FindFilenameNodesInFST(nodes_out, filename,
&std::get<std::vector<FSTBuilderNode>>(node.m_content));
}
else if (node.m_filename == filename)
{
nodes_out->push_back(&node);
}
}
}
static void ApplyFolderPatchToFST(const Patch& patch, const Folder& folder,
const std::vector<FileDataLoader::Node>& external_files,
const std::string& external_path, bool recursive,
std::string_view disc_path,
std::vector<DiscIO::FSTBuilderNode>* fst)
{
for (const auto& child : external_files)
{
const std::string child_disc_path = std::string(disc_path) + "/" + child.m_filename;
const std::string child_external_path = external_path + "/" + child.m_filename;
if (child.m_is_directory)
{
if (recursive)
{
ApplyFolderPatchToFST(patch, folder,
patch.m_file_data_loader->GetFolderContents(child_external_path),
child_external_path, recursive, child_disc_path, fst);
}
}
else
{
DiscIO::FSTBuilderNode* node = FindFileNodeInFST(child_disc_path, fst, folder.m_create);
if (node)
ApplyPatchToFile(patch, node, child_external_path, 0, 0, folder.m_length, folder.m_resize);
}
}
}
static void ApplyUnknownFolderPatchToFST(const Patch& patch, const Folder& folder,
const std::vector<FileDataLoader::Node>& external_files,
const std::string& external_path, bool recursive,
std::vector<DiscIO::FSTBuilderNode>* fst)
{
for (const auto& child : external_files)
{
const std::string child_external_path = external_path + "/" + child.m_filename;
if (child.m_is_directory)
{
if (recursive)
{
ApplyUnknownFolderPatchToFST(
patch, folder, patch.m_file_data_loader->GetFolderContents(child_external_path),
child_external_path, recursive, fst);
}
}
else
{
std::vector<DiscIO::FSTBuilderNode*> nodes;
FindFilenameNodesInFST(&nodes, child.m_filename, fst);
for (auto* node : nodes)
ApplyPatchToFile(patch, node, child_external_path, 0, 0, folder.m_length, folder.m_resize);
}
}
}
void ApplyPatchesToFiles(const std::vector<Patch>& patches,
std::vector<DiscIO::FSTBuilderNode>* fst, DiscIO::FSTBuilderNode* dol_node)
{
// For file searching purposes, Riivolution assumes that the game's main.dol is in the root of the
// file system. So to avoid doing a bunch of special case handling for that, we just put a node
// for this into the FST and remove it again after the file patching is done.
// We mark the inserted node with a pointer to a stack variable so we can find it again.
int marker = 0;
fst->emplace_back(DiscIO::FSTBuilderNode{"main.dol", dol_node->m_size,
std::move(dol_node->m_content), &marker});
for (const auto& patch : patches)
{
for (const auto& file : patch.m_file_patches)
{
if (!file.m_disc.empty() && file.m_disc[0] == '/')
{
// If the disc path starts with a / then we should patch that specific disc path.
DiscIO::FSTBuilderNode* node =
FindFileNodeInFST(std::string_view(file.m_disc).substr(1), fst, file.m_create);
if (node)
ApplyPatchToFile(patch, file, node);
}
else
{
// Otherwise we want to patch any file on the entire disc matching that filename.
std::vector<DiscIO::FSTBuilderNode*> nodes;
FindFilenameNodesInFST(&nodes, file.m_disc, fst);
for (auto* node : nodes)
ApplyPatchToFile(patch, file, node);
}
}
for (const auto& folder : patch.m_folder_patches)
{
const auto external_files = patch.m_file_data_loader->GetFolderContents(folder.m_external);
std::string_view disc_path = folder.m_disc;
while (StringBeginsWith(disc_path, "/"))
disc_path.remove_prefix(1);
while (StringEndsWith(disc_path, "/"))
disc_path.remove_suffix(1);
if (disc_path.empty())
{
ApplyUnknownFolderPatchToFST(patch, folder, external_files, folder.m_external,
folder.m_recursive, fst);
}
else
{
ApplyFolderPatchToFST(patch, folder, external_files, folder.m_external, folder.m_recursive,
disc_path, fst);
}
}
}
// Remove the inserted main.dol node again and propagate its changes.
auto main_dol_node_in_fst =
std::find_if(fst->begin(), fst->end(),
[&](const DiscIO::FSTBuilderNode& node) { return node.m_user_data == &marker; });
if (main_dol_node_in_fst != fst->end())
{
dol_node->m_size = main_dol_node_in_fst->m_size;
dol_node->m_content = std::move(main_dol_node_in_fst->m_content);
fst->erase(main_dol_node_in_fst);
}
else
{
// The main.dol node disappeared, this should never happen.
ASSERT(false);
}
}
static bool MemoryMatchesAt(u32 offset, const std::vector<u8>& value)
{
for (u32 i = 0; i < value.size(); ++i)
{
auto result = PowerPC::HostTryReadU8(offset + i);
if (!result || result->value != value[i])
return false;
}
return true;
}
static void ApplyMemoryPatch(u32 offset, const std::vector<u8>& value,
const std::vector<u8>& original)
{
if (value.empty())
return;
if (!original.empty() && !MemoryMatchesAt(offset, original))
return;
for (u32 i = 0; i < value.size(); ++i)
PowerPC::HostTryWriteU8(value[i], offset + i);
}
static std::vector<u8> GetMemoryPatchValue(const Patch& patch, const Memory& memory_patch)
{
if (!memory_patch.m_valuefile.empty())
return patch.m_file_data_loader->GetFileContents(memory_patch.m_valuefile);
return memory_patch.m_value;
}
static void ApplyMemoryPatch(const Patch& patch, const Memory& memory_patch)
{
if (memory_patch.m_offset == 0)
return;
ApplyMemoryPatch(memory_patch.m_offset | 0x80000000, GetMemoryPatchValue(patch, memory_patch),
memory_patch.m_original);
}
static void ApplySearchMemoryPatch(const Patch& patch, const Memory& memory_patch, u32 ram_start,
u32 length)
{
if (memory_patch.m_original.empty() || memory_patch.m_align == 0)
return;
const u32 stride = memory_patch.m_align;
for (u32 i = 0; i < length - (stride - 1); i += stride)
{
const u32 address = ram_start + i;
if (MemoryMatchesAt(address, memory_patch.m_original))
{
ApplyMemoryPatch(address, GetMemoryPatchValue(patch, memory_patch), {});
break;
}
}
}
static void ApplyOcarinaMemoryPatch(const Patch& patch, const Memory& memory_patch, u32 ram_start,
u32 length)
{
if (memory_patch.m_offset == 0)
return;
const std::vector<u8> value = GetMemoryPatchValue(patch, memory_patch);
if (value.empty())
return;
for (u32 i = 0; i < length; i += 4)
{
// first find the pattern
const u32 address = ram_start + i;
if (MemoryMatchesAt(address, value))
{
for (; i < length; i += 4)
{
// from the pattern find the next blr instruction
const u32 blr_address = ram_start + i;
auto blr = PowerPC::HostTryReadU32(blr_address);
if (blr && blr->value == 0x4e800020)
{
// and replace it with a jump to the given offset
const u32 target = memory_patch.m_offset | 0x80000000;
const u32 jmp = ((target - blr_address) & 0x03fffffc) | 0x48000000;
PowerPC::HostTryWriteU32(jmp, blr_address);
return;
}
}
return;
}
}
}
void ApplyGeneralMemoryPatches(const std::vector<Patch>& patches)
{
for (const auto& patch : patches)
{
for (const auto& memory : patch.m_memory_patches)
{
if (memory.m_ocarina)
continue;
if (memory.m_search)
ApplySearchMemoryPatch(patch, memory, 0x80000000, ::Memory::GetRamSize());
else
ApplyMemoryPatch(patch, memory);
}
}
}
void ApplyApploaderMemoryPatches(const std::vector<Patch>& patches, u32 ram_address, u32 ram_length)
{
for (const auto& patch : patches)
{
for (const auto& memory : patch.m_memory_patches)
{
if (!memory.m_ocarina && !memory.m_search)
continue;
if (memory.m_ocarina)
ApplyOcarinaMemoryPatch(patch, memory, ram_address, ram_length);
else
ApplySearchMemoryPatch(patch, memory, ram_address, ram_length);
}
}
}
std::optional<SavegameRedirect>
ExtractSavegameRedirect(const std::vector<Patch>& riivolution_patches)
{
for (const auto& patch : riivolution_patches)
{
if (!patch.m_savegame_patches.empty())
{
const auto& save_patch = patch.m_savegame_patches[0];
auto resolved = patch.m_file_data_loader->ResolveSavegameRedirectPath(save_patch.m_external);
if (resolved)
return SavegameRedirect{std::move(*resolved), save_patch.m_clone};
return std::nullopt;
}
}
return std::nullopt;
}
} // namespace DiscIO::Riivolution

View File

@ -0,0 +1,76 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "DiscIO/DirectoryBlob.h"
#include "DiscIO/RiivolutionParser.h"
namespace DiscIO::Riivolution
{
struct SavegameRedirect
{
std::string m_target_path;
bool m_clone;
};
class FileDataLoader
{
public:
struct Node
{
std::string m_filename;
bool m_is_directory;
};
virtual ~FileDataLoader();
virtual std::optional<u64> GetExternalFileSize(std::string_view external_relative_path) = 0;
virtual std::vector<u8> GetFileContents(std::string_view external_relative_path) = 0;
virtual std::vector<Node> GetFolderContents(std::string_view external_relative_path) = 0;
virtual BuilderContentSource MakeContentSource(std::string_view external_relative_path,
u64 external_offset, u64 external_size,
u64 disc_offset) = 0;
virtual std::optional<std::string>
ResolveSavegameRedirectPath(std::string_view external_relative_path) = 0;
};
class FileDataLoaderHostFS : public FileDataLoader
{
public:
// sd_root should be an absolute path to the folder representing our virtual SD card
// xml_path should be an absolute path to the parsed XML file
// patch_root should be the 'root' attribute given in the 'patch' or 'wiiroot' XML element
FileDataLoaderHostFS(std::string sd_root, const std::string& xml_path,
std::string_view patch_root);
std::optional<u64> GetExternalFileSize(std::string_view external_relative_path) override;
std::vector<u8> GetFileContents(std::string_view external_relative_path) override;
std::vector<FileDataLoader::Node>
GetFolderContents(std::string_view external_relative_path) override;
BuilderContentSource MakeContentSource(std::string_view external_relative_path,
u64 external_offset, u64 external_size,
u64 disc_offset) override;
std::optional<std::string>
ResolveSavegameRedirectPath(std::string_view external_relative_path) override;
private:
std::optional<std::string> MakeAbsoluteFromRelative(std::string_view external_relative_path);
std::string m_sd_root;
std::string m_patch_root;
};
void ApplyPatchesToFiles(const std::vector<Patch>& patches,
std::vector<DiscIO::FSTBuilderNode>* fst,
DiscIO::FSTBuilderNode* dol_node);
void ApplyGeneralMemoryPatches(const std::vector<Patch>& patches);
void ApplyApploaderMemoryPatches(const std::vector<Patch>& patches, u32 ram_address,
u32 ram_length);
std::optional<SavegameRedirect>
ExtractSavegameRedirect(const std::vector<Patch>& riivolution_patches);
} // namespace DiscIO::Riivolution

View File

@ -85,8 +85,11 @@ std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t>
return names; return names;
} }
static std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader>& reader) static std::unique_ptr<VolumeDisc> TryCreateDisc(std::unique_ptr<BlobReader>& reader)
{ {
if (!reader)
return nullptr;
if (reader->ReadSwapped<u32>(0x18) == WII_DISC_MAGIC) if (reader->ReadSwapped<u32>(0x18) == WII_DISC_MAGIC)
return std::make_unique<VolumeWii>(std::move(reader)); return std::make_unique<VolumeWii>(std::move(reader));
@ -97,14 +100,21 @@ static std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader>& reade
return nullptr; return nullptr;
} }
std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path) std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader> reader)
{ {
std::unique_ptr<BlobReader> reader(CreateBlobReader(path)); return TryCreateDisc(reader);
return reader ? CreateDisc(reader) : nullptr;
} }
static std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader>& reader) std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path)
{ {
return CreateDisc(CreateBlobReader(path));
}
static std::unique_ptr<VolumeWAD> TryCreateWAD(std::unique_ptr<BlobReader>& reader)
{
if (!reader)
return nullptr;
// Check for WAD // Check for WAD
// 0x206962 for boot2 wads // 0x206962 for boot2 wads
const std::optional<u32> wad_magic = reader->ReadSwapped<u32>(0x02); const std::optional<u32> wad_magic = reader->ReadSwapped<u32>(0x02);
@ -115,27 +125,31 @@ static std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader>& reader)
return nullptr; return nullptr;
} }
std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path) std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader> reader)
{ {
std::unique_ptr<BlobReader> reader(CreateBlobReader(path)); return TryCreateWAD(reader);
return reader ? CreateWAD(reader) : nullptr;
} }
std::unique_ptr<Volume> CreateVolume(const std::string& path) std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path)
{ {
std::unique_ptr<BlobReader> reader(CreateBlobReader(path)); return CreateWAD(CreateBlobReader(path));
if (reader == nullptr) }
return nullptr;
std::unique_ptr<VolumeDisc> disc = CreateDisc(reader); std::unique_ptr<Volume> CreateVolume(std::unique_ptr<BlobReader> reader)
{
std::unique_ptr<VolumeDisc> disc = TryCreateDisc(reader);
if (disc) if (disc)
return disc; return disc;
std::unique_ptr<VolumeWAD> wad = CreateWAD(reader); std::unique_ptr<VolumeWAD> wad = TryCreateWAD(reader);
if (wad) if (wad)
return wad; return wad;
return nullptr; return nullptr;
} }
std::unique_ptr<Volume> CreateVolume(const std::string& path)
{
return CreateVolume(CreateBlobReader(path));
}
} // namespace DiscIO } // namespace DiscIO

View File

@ -182,8 +182,11 @@ protected:
static const std::vector<u8> INVALID_CERT_CHAIN; static const std::vector<u8> INVALID_CERT_CHAIN;
}; };
std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader> reader);
std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path); std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path);
std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader> reader);
std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path); std::unique_ptr<VolumeWAD> CreateWAD(const std::string& path);
std::unique_ptr<Volume> CreateVolume(std::unique_ptr<BlobReader> reader);
std::unique_ptr<Volume> CreateVolume(const std::string& path); std::unique_ptr<Volume> CreateVolume(const std::string& path);
} // namespace DiscIO } // namespace DiscIO

View File

@ -430,9 +430,12 @@
<ClInclude Include="DiscIO\FileBlob.h" /> <ClInclude Include="DiscIO\FileBlob.h" />
<ClInclude Include="DiscIO\Filesystem.h" /> <ClInclude Include="DiscIO\Filesystem.h" />
<ClInclude Include="DiscIO\FileSystemGCWii.h" /> <ClInclude Include="DiscIO\FileSystemGCWii.h" />
<ClInclude Include="DiscIO\GameModDescriptor.h" />
<ClInclude Include="DiscIO\LaggedFibonacciGenerator.h" /> <ClInclude Include="DiscIO\LaggedFibonacciGenerator.h" />
<ClInclude Include="DiscIO\MultithreadedCompressor.h" /> <ClInclude Include="DiscIO\MultithreadedCompressor.h" />
<ClInclude Include="DiscIO\NANDImporter.h" /> <ClInclude Include="DiscIO\NANDImporter.h" />
<ClInclude Include="DiscIO\RiivolutionParser.h" />
<ClInclude Include="DiscIO\RiivolutionPatcher.h" />
<ClInclude Include="DiscIO\ScrubbedBlob.h" /> <ClInclude Include="DiscIO\ScrubbedBlob.h" />
<ClInclude Include="DiscIO\TGCBlob.h" /> <ClInclude Include="DiscIO\TGCBlob.h" />
<ClInclude Include="DiscIO\Volume.h" /> <ClInclude Include="DiscIO\Volume.h" />
@ -1017,8 +1020,11 @@
<ClCompile Include="DiscIO\FileBlob.cpp" /> <ClCompile Include="DiscIO\FileBlob.cpp" />
<ClCompile Include="DiscIO\Filesystem.cpp" /> <ClCompile Include="DiscIO\Filesystem.cpp" />
<ClCompile Include="DiscIO\FileSystemGCWii.cpp" /> <ClCompile Include="DiscIO\FileSystemGCWii.cpp" />
<ClCompile Include="DiscIO\GameModDescriptor.cpp" />
<ClCompile Include="DiscIO\LaggedFibonacciGenerator.cpp" /> <ClCompile Include="DiscIO\LaggedFibonacciGenerator.cpp" />
<ClCompile Include="DiscIO\NANDImporter.cpp" /> <ClCompile Include="DiscIO\NANDImporter.cpp" />
<ClCompile Include="DiscIO\RiivolutionParser.cpp" />
<ClCompile Include="DiscIO\RiivolutionPatcher.cpp" />
<ClCompile Include="DiscIO\ScrubbedBlob.cpp" /> <ClCompile Include="DiscIO\ScrubbedBlob.cpp" />
<ClCompile Include="DiscIO\TGCBlob.cpp" /> <ClCompile Include="DiscIO\TGCBlob.cpp" />
<ClCompile Include="DiscIO\Volume.cpp" /> <ClCompile Include="DiscIO\Volume.cpp" />

View File

@ -291,6 +291,8 @@ add_executable(dolphin-emu
QtUtils/AspectRatioWidget.h QtUtils/AspectRatioWidget.h
ResourcePackManager.cpp ResourcePackManager.cpp
ResourcePackManager.h ResourcePackManager.h
RiivolutionBootWidget.cpp
RiivolutionBootWidget.h
Settings/AdvancedPane.cpp Settings/AdvancedPane.cpp
Settings/AdvancedPane.h Settings/AdvancedPane.h
Settings/AudioPane.cpp Settings/AudioPane.cpp

View File

@ -179,6 +179,7 @@
<ClCompile Include="RenderWidget.cpp" /> <ClCompile Include="RenderWidget.cpp" />
<ClCompile Include="ResourcePackManager.cpp" /> <ClCompile Include="ResourcePackManager.cpp" />
<ClCompile Include="Resources.cpp" /> <ClCompile Include="Resources.cpp" />
<ClCompile Include="RiivolutionBootWidget.cpp" />
<ClCompile Include="SearchBar.cpp" /> <ClCompile Include="SearchBar.cpp" />
<ClCompile Include="Settings.cpp" /> <ClCompile Include="Settings.cpp" />
<ClCompile Include="Settings\AdvancedPane.cpp" /> <ClCompile Include="Settings\AdvancedPane.cpp" />
@ -351,6 +352,7 @@
<QtMoc Include="QtUtils\UTF8CodePointCountValidator.h" /> <QtMoc Include="QtUtils\UTF8CodePointCountValidator.h" />
<QtMoc Include="QtUtils\WindowActivationEventFilter.h" /> <QtMoc Include="QtUtils\WindowActivationEventFilter.h" />
<QtMoc Include="RenderWidget.h" /> <QtMoc Include="RenderWidget.h" />
<QtMoc Include="RiivolutionBootWidget.h" />
<QtMoc Include="SearchBar.h" /> <QtMoc Include="SearchBar.h" />
<QtMoc Include="Settings.h" /> <QtMoc Include="Settings.h" />
<QtMoc Include="Settings\AdvancedPane.h" /> <QtMoc Include="Settings\AdvancedPane.h" />

View File

@ -353,6 +353,11 @@ void GameList::ShowContextMenu(const QPoint&)
if (DiscIO::IsDisc(platform)) if (DiscIO::IsDisc(platform))
{ {
menu->addAction(tr("Start with Riivolution Patches..."), this,
&GameList::StartWithRiivolution);
menu->addSeparator();
menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO); menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO);
if (game->ShouldAllowConversion()) if (game->ShouldAllowConversion())
@ -587,6 +592,15 @@ void GameList::UninstallWAD()
result_dialog.exec(); result_dialog.exec();
} }
void GameList::StartWithRiivolution()
{
const auto game = GetSelectedGame();
if (!game)
return;
emit OnStartWithRiivolution(*game);
}
void GameList::SetDefaultISO() void GameList::SetDefaultISO()
{ {
const auto game = GetSelectedGame(); const auto game = GetSelectedGame();

View File

@ -50,6 +50,7 @@ public:
signals: signals:
void GameSelected(); void GameSelected();
void OnStartWithRiivolution(const UICommon::GameFile& game);
void NetPlayHost(const UICommon::GameFile& game); void NetPlayHost(const UICommon::GameFile& game);
void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file); void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file);
void OpenGeneralSettings(); void OpenGeneralSettings();
@ -62,6 +63,7 @@ private:
void OpenWiiSaveFolder(); void OpenWiiSaveFolder();
void OpenGCSaveFolder(); void OpenGCSaveFolder();
void OpenWiki(); void OpenWiki();
void StartWithRiivolution();
void SetDefaultISO(); void SetDefaultISO();
void DeleteFile(); void DeleteFile();
#ifdef _WIN32 #ifdef _WIN32

View File

@ -58,7 +58,9 @@
#include "Core/State.h" #include "Core/State.h"
#include "Core/WiiUtils.h" #include "Core/WiiUtils.h"
#include "DiscIO/DirectoryBlob.h"
#include "DiscIO/NANDImporter.h" #include "DiscIO/NANDImporter.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "DolphinQt/AboutDialog.h" #include "DolphinQt/AboutDialog.h"
#include "DolphinQt/CheatsManager.h" #include "DolphinQt/CheatsManager.h"
@ -100,6 +102,7 @@
#include "DolphinQt/RenderWidget.h" #include "DolphinQt/RenderWidget.h"
#include "DolphinQt/ResourcePackManager.h" #include "DolphinQt/ResourcePackManager.h"
#include "DolphinQt/Resources.h" #include "DolphinQt/Resources.h"
#include "DolphinQt/RiivolutionBootWidget.h"
#include "DolphinQt/SearchBar.h" #include "DolphinQt/SearchBar.h"
#include "DolphinQt/Settings.h" #include "DolphinQt/Settings.h"
#include "DolphinQt/TAS/GCTASInputWindow.h" #include "DolphinQt/TAS/GCTASInputWindow.h"
@ -643,6 +646,8 @@ void MainWindow::ConnectGameList()
{ {
connect(m_game_list, &GameList::GameSelected, this, [this]() { Play(); }); connect(m_game_list, &GameList::GameSelected, this, [this]() { Play(); });
connect(m_game_list, &GameList::NetPlayHost, this, &MainWindow::NetPlayHost); connect(m_game_list, &GameList::NetPlayHost, this, &MainWindow::NetPlayHost);
connect(m_game_list, &GameList::OnStartWithRiivolution, this,
&MainWindow::ShowRiivolutionBootWidget);
connect(m_game_list, &GameList::OpenGeneralSettings, this, &MainWindow::ShowGeneralWindow); connect(m_game_list, &GameList::OpenGeneralSettings, this, &MainWindow::ShowGeneralWindow);
} }
@ -1807,6 +1812,30 @@ void MainWindow::ShowCheatsManager()
m_cheats_manager->show(); m_cheats_manager->show();
} }
void MainWindow::ShowRiivolutionBootWidget(const UICommon::GameFile& game)
{
auto second_game = m_game_list->FindSecondDisc(game);
std::vector<std::string> paths = {game.GetFilePath()};
if (second_game != nullptr)
paths.push_back(second_game->GetFilePath());
std::unique_ptr<BootParameters> boot_params =
BootParameters::GenerateFromFile(paths, std::nullopt);
if (!boot_params)
return;
if (!std::holds_alternative<BootParameters::Disc>(boot_params->parameters))
return;
auto& disc = std::get<BootParameters::Disc>(boot_params->parameters);
RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(),
disc.volume->GetDiscNumber(), this);
w.exec();
if (!w.ShouldBoot())
return;
AddRiivolutionPatches(boot_params.get(), std::move(w.GetPatches()));
StartGame(std::move(boot_params));
}
void MainWindow::Show() void MainWindow::Show()
{ {
if (!Settings::Instance().IsBatchModeEnabled()) if (!Settings::Instance().IsBatchModeEnabled())

View File

@ -157,6 +157,7 @@ private:
void ShowMemcardManager(); void ShowMemcardManager();
void ShowResourcePackManager(); void ShowResourcePackManager();
void ShowCheatsManager(); void ShowCheatsManager();
void ShowRiivolutionBootWidget(const UICommon::GameFile& game);
void NetPlayInit(); void NetPlayInit();
bool NetPlayJoin(); bool NetPlayJoin();

View File

@ -0,0 +1,281 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/RiivolutionBootWidget.h"
#include <unordered_map>
#include <fmt/format.h>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMetaType>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include "Common/FileSearch.h"
#include "Common/FileUtil.h"
#include "DiscIO/RiivolutionParser.h"
#include "DiscIO/RiivolutionPatcher.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
struct GuiRiivolutionPatchIndex
{
size_t m_disc_index;
size_t m_section_index;
size_t m_option_index;
size_t m_choice_index;
};
Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex);
RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
std::optional<u8> disc, QWidget* parent)
: QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc)
{
setWindowTitle(tr("Start with Riivolution Patches"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateWidgets();
LoadMatchingXMLs();
resize(QSize(400, 600));
}
RiivolutionBootWidget::~RiivolutionBootWidget() = default;
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* group_box = new QGroupBox();
auto* scroll_area = new QScrollArea();
auto* stretch_helper = new QVBoxLayout();
m_patch_section_layout = new QVBoxLayout();
stretch_helper->addLayout(m_patch_section_layout);
stretch_helper->addStretch();
group_box->setLayout(stretch_helper);
scroll_area->setWidget(group_box);
scroll_area->setWidgetResizable(true);
auto* button_layout = new QHBoxLayout();
button_layout->addStretch();
button_layout->addWidget(open_xml_button, 0, Qt::AlignRight);
button_layout->addWidget(boot_game_button, 0, Qt::AlignRight);
auto* layout = new QVBoxLayout();
layout->addWidget(scroll_area);
layout->addLayout(button_layout);
setLayout(layout);
connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML);
connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame);
}
void RiivolutionBootWidget::LoadMatchingXMLs()
{
const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX);
const auto config = LoadConfigXML(riivolution_dir);
for (const std::string& path : Common::DoFileSearch({riivolution_dir + "riivolution"}, {".xml"}))
{
auto parsed = DiscIO::Riivolution::ParseFile(path);
if (!parsed || !parsed->IsValidForGame(m_game_id, m_revision, m_disc_number))
continue;
if (config)
DiscIO::Riivolution::ApplyConfigDefaults(&*parsed, *config);
MakeGUIForParsedFile(path, riivolution_dir, *parsed);
}
}
static std::string FindRoot(const std::string& path)
{
// Try to set the virtual SD root to directory one up from current.
// This mimics where the XML would be on a real SD card.
QDir dir = QFileInfo(QString::fromStdString(path)).dir();
if (dir.cdUp())
return dir.absolutePath().toStdString();
return File::GetUserPath(D_RIIVOLUTION_IDX);
}
void RiivolutionBootWidget::OpenXML()
{
const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX);
QStringList paths = QFileDialog::getOpenFileNames(
this, tr("Select Riivolution XML file"), QString::fromStdString(riivolution_dir),
QStringLiteral("%1 (*.xml);;%2 (*)").arg(tr("Riivolution XML files")).arg(tr("All Files")));
if (paths.isEmpty())
return;
for (const QString& path : paths)
{
std::string p = path.toStdString();
auto parsed = DiscIO::Riivolution::ParseFile(p);
if (!parsed)
{
ModalMessageBox::warning(
this, tr("Failed loading XML."),
tr("Did not recognize %1 as a valid Riivolution XML file.").arg(path));
continue;
}
if (!parsed->IsValidForGame(m_game_id, m_revision, m_disc_number))
{
ModalMessageBox::warning(
this, tr("Invalid game."),
tr("The patches in %1 are not for the selected game or game revision.").arg(path));
continue;
}
auto root = FindRoot(p);
const auto config = LoadConfigXML(root);
if (config)
DiscIO::Riivolution::ApplyConfigDefaults(&*parsed, *config);
MakeGUIForParsedFile(p, std::move(root), *parsed);
}
}
void RiivolutionBootWidget::MakeGUIForParsedFile(const 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)});
auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(path)).fileName());
auto* disc_layout = new QVBoxLayout();
disc_box->setLayout(disc_layout);
auto* xml_root_line_edit = new QLineEdit(QString::fromStdString(disc.root));
xml_root_line_edit->setReadOnly(true);
auto* xml_root_layout = new QHBoxLayout();
auto* xml_root_open = new QPushButton(tr("..."));
xml_root_layout->addWidget(new QLabel(tr("SD Root:")), 0);
xml_root_layout->addWidget(xml_root_line_edit, 0);
xml_root_layout->addWidget(xml_root_open, 0);
disc_layout->addLayout(xml_root_layout);
connect(xml_root_open, &QPushButton::clicked, this, [this, xml_root_line_edit, disc_index]() {
QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(
this, tr("Select the Virtual SD Card Root"), xml_root_line_edit->text()));
if (!dir.isEmpty())
{
xml_root_line_edit->setText(dir);
m_discs[disc_index].root = dir.toStdString();
}
});
for (size_t section_index = 0; section_index < disc.disc.m_sections.size(); ++section_index)
{
const auto& section = disc.disc.m_sections[section_index];
auto* group_box = new QGroupBox(QString::fromStdString(section.m_name));
auto* grid_layout = new QGridLayout();
group_box->setLayout(grid_layout);
int row = 0;
for (size_t option_index = 0; option_index < section.m_options.size(); ++option_index)
{
const auto& option = section.m_options[option_index];
auto* label = new QLabel(QString::fromStdString(option.m_name));
auto* selection = new QComboBox();
const GuiRiivolutionPatchIndex gui_disabled_index{disc_index, section_index, option_index, 0};
selection->addItem(tr("Disabled"), QVariant::fromValue(gui_disabled_index));
for (size_t choice_index = 0; choice_index < option.m_choices.size(); ++choice_index)
{
const auto& choice = option.m_choices[choice_index];
const GuiRiivolutionPatchIndex gui_index{disc_index, section_index, option_index,
choice_index + 1};
selection->addItem(QString::fromStdString(choice.m_name), QVariant::fromValue(gui_index));
}
if (option.m_selected_choice <= option.m_choices.size())
selection->setCurrentIndex(static_cast<int>(option.m_selected_choice));
connect(selection, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this, selection](int idx) {
const auto gui_index = selection->currentData().value<GuiRiivolutionPatchIndex>();
auto& disc = m_discs[gui_index.m_disc_index].disc;
auto& section = disc.m_sections[gui_index.m_section_index];
auto& option = section.m_options[gui_index.m_option_index];
option.m_selected_choice = static_cast<u32>(gui_index.m_choice_index);
});
grid_layout->addWidget(label, row, 0, 1, 1);
grid_layout->addWidget(selection, row, 1, 1, 1);
++row;
}
disc_layout->addWidget(group_box);
}
m_patch_section_layout->addWidget(disc_box);
}
std::optional<DiscIO::Riivolution::Config>
RiivolutionBootWidget::LoadConfigXML(const std::string& root_directory)
{
// The way Riivolution stores settings only makes sense for standard game IDs.
if (!(m_game_id.size() == 4 || m_game_id.size() == 6))
return std::nullopt;
return DiscIO::Riivolution::ParseConfigFile(
fmt::format("{}/riivolution/config/{}.xml", root_directory, m_game_id.substr(0, 4)));
}
void RiivolutionBootWidget::SaveConfigXMLs()
{
if (!(m_game_id.size() == 4 || m_game_id.size() == 6))
return;
std::unordered_map<std::string, DiscIO::Riivolution::Config> map;
for (const auto& disc : m_discs)
{
auto config = map.try_emplace(disc.root);
auto& config_options = config.first->second.m_options;
for (const auto& section : disc.disc.m_sections)
{
for (const auto& option : section.m_options)
{
std::string id = option.m_id.empty() ? (section.m_name + option.m_name) : option.m_id;
config_options.emplace_back(
DiscIO::Riivolution::ConfigOption{std::move(id), option.m_selected_choice});
}
}
}
for (const auto& config : map)
{
DiscIO::Riivolution::WriteConfigFile(
fmt::format("{}/riivolution/config/{}.xml", config.first, m_game_id.substr(0, 4)),
config.second);
}
}
void RiivolutionBootWidget::BootGame()
{
SaveConfigXMLs();
m_patches.clear();
for (const auto& disc : m_discs)
{
auto patches = disc.disc.GeneratePatches(m_game_id);
// set the file loader for each patch
for (auto& patch : patches)
{
patch.m_file_data_loader = std::make_shared<DiscIO::Riivolution::FileDataLoaderHostFS>(
disc.root, disc.disc.m_xml_path, patch.m_root);
}
m_patches.insert(m_patches.end(), patches.begin(), patches.end());
}
m_should_boot = true;
close();
}

View File

@ -0,0 +1,53 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string>
#include <QDialog>
#include "Common/CommonTypes.h"
#include "DiscIO/RiivolutionParser.h"
class QPushButton;
class QVBoxLayout;
class RiivolutionBootWidget : public QDialog
{
Q_OBJECT
public:
explicit RiivolutionBootWidget(std::string game_id, std::optional<u16> revision,
std::optional<u8> disc, QWidget* parent = nullptr);
~RiivolutionBootWidget();
bool ShouldBoot() const { return m_should_boot; }
std::vector<DiscIO::Riivolution::Patch>& GetPatches() { return m_patches; }
private:
void CreateWidgets();
void LoadMatchingXMLs();
void OpenXML();
void MakeGUIForParsedFile(const std::string& path, std::string root,
DiscIO::Riivolution::Disc input_disc);
std::optional<DiscIO::Riivolution::Config> LoadConfigXML(const std::string& root_directory);
void SaveConfigXMLs();
void BootGame();
std::string m_game_id;
std::optional<u16> m_revision;
std::optional<u8> m_disc_number;
bool m_should_boot = false;
struct DiscWithRoot
{
DiscIO::Riivolution::Disc disc;
std::string root;
};
std::vector<DiscWithRoot> m_discs;
std::vector<DiscIO::Riivolution::Patch> m_patches;
QVBoxLayout* m_patch_section_layout;
};

View File

@ -67,6 +67,7 @@ static void CreateLoadPath(const std::string& path)
if (!path.empty()) if (!path.empty())
File::SetUserPath(D_LOAD_IDX, path + '/'); File::SetUserPath(D_LOAD_IDX, path + '/');
File::CreateFullPath(File::GetUserPath(D_HIRESTEXTURES_IDX)); File::CreateFullPath(File::GetUserPath(D_HIRESTEXTURES_IDX));
File::CreateFullPath(File::GetUserPath(D_RIIVOLUTION_IDX));
} }
static void CreateResourcePackPath(const std::string& path) static void CreateResourcePackPath(const std::string& path)