Merge pull request #10127 from AdmiralCurtiss/riivolution
HLE Riivolution patch support
This commit is contained in:
commit
5d5f019921
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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{};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, ¤t_data_address, root_offset,
|
WriteDirectory(&root_nodes, &fst_offset, &name_offset, ¤t_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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 == ▮ });
|
||||||
|
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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue