80 changes: 60 additions & 20 deletions Source/Core/Core/IOS/FS/HostBackend/FS.cpp
Expand Up @@ -22,13 +22,24 @@

namespace IOS::HLE::FS
{
std::string HostFileSystem::BuildFilename(const std::string& wii_path) const
HostFileSystem::HostFilename HostFileSystem::BuildFilename(const std::string& wii_path) const
{
for (const auto& redirect : m_nand_redirects)
{
if (StringBeginsWith(wii_path, redirect.source_path) &&
(wii_path.size() == redirect.source_path.size() ||
wii_path[redirect.source_path.size()] == '/'))
{
std::string relative_to_redirect = wii_path.substr(redirect.source_path.size());
return HostFilename{redirect.target_path + Common::EscapePath(relative_to_redirect), true};
}
}

if (wii_path.compare(0, 1, "/") == 0)
return m_root_path + Common::EscapePath(wii_path);
return HostFilename{m_root_path + Common::EscapePath(wii_path), false};

ASSERT(false);
return m_root_path;
return HostFilename{m_root_path, false};
}

// Get total filesize of contents of a directory (recursive)
Expand Down Expand Up @@ -101,7 +112,9 @@ bool HostFileSystem::FstEntry::CheckPermission(Uid caller_uid, Gid caller_gid,
return (u8(requested_mode) & u8(file_mode)) == u8(requested_mode);
}

HostFileSystem::HostFileSystem(const std::string& root_path) : m_root_path{root_path}
HostFileSystem::HostFileSystem(const std::string& root_path,
std::vector<NandRedirect> nand_redirects)
: m_root_path{root_path}, m_nand_redirects(std::move(nand_redirects))
{
File::CreateFullPath(m_root_path + "/");
ResetFst();
Expand Down Expand Up @@ -197,11 +210,12 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
if (!IsValidNonRootPath(path))
return nullptr;

const File::FileInfo host_file_info{BuildFilename(path)};
auto host_file = BuildFilename(path);
const File::FileInfo host_file_info{host_file.host_path};
if (!host_file_info.Exists())
return nullptr;

FstEntry* entry = &m_root_entry;
FstEntry* entry = host_file.is_redirect ? &m_redirect_fst : &m_root_entry;
std::string complete_path = "";
for (const std::string& component : SplitString(std::string(path.substr(1)), '/'))
{
Expand All @@ -217,7 +231,8 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
// Fall back to dummy data to avoid breaking existing filesystems.
// This code path is also reached when creating a new file or directory;
// proper metadata is filled in later.
INFO_LOG_FMT(IOS_FS, "Creating a default entry for {}", complete_path);
INFO_LOG_FMT(IOS_FS, "Creating a default entry for {} ({})", complete_path,
host_file.is_redirect ? "redirect" : "NAND");
entry = &entry->children.emplace_back();
entry->name = component;
entry->data.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite};
Expand All @@ -241,7 +256,7 @@ void HostFileSystem::DoState(PointerWrap& p)
handle.host_file.reset();

// handle /tmp
std::string Path = BuildFilename("/tmp");
std::string Path = BuildFilename("/tmp").host_path;
if (p.GetMode() == PointerWrap::MODE_READ)
{
File::DeleteDirRecursively(Path);
Expand Down Expand Up @@ -336,7 +351,7 @@ void HostFileSystem::DoState(PointerWrap& p)
p.Do(handle.wii_path);
p.Do(handle.file_offset);
if (handle.opened)
handle.host_file = OpenHostFile(BuildFilename(handle.wii_path));
handle.host_file = OpenHostFile(BuildFilename(handle.wii_path).host_path);
}
}

Expand All @@ -346,7 +361,7 @@ ResultCode HostFileSystem::Format(Uid uid)
return ResultCode::AccessDenied;
if (m_root_path.empty())
return ResultCode::AccessDenied;
const std::string root = BuildFilename("/");
const std::string root = BuildFilename("/").host_path;
if (!File::DeleteDirRecursively(root) || !File::CreateDir(root))
return ResultCode::UnknownError;
ResetFst();
Expand All @@ -366,7 +381,7 @@ ResultCode HostFileSystem::CreateFileOrDirectory(Uid uid, Gid gid, const std::st
return ResultCode::TooManyPathComponents;

const auto split_path = SplitPathAndBasename(path);
const std::string host_path = BuildFilename(path);
const std::string host_path = BuildFilename(path).host_path;

FstEntry* parent = GetFstEntryForPath(split_path.parent);
if (!parent)
Expand Down Expand Up @@ -428,7 +443,7 @@ ResultCode HostFileSystem::Delete(Uid uid, Gid gid, const std::string& path)
if (!IsValidNonRootPath(path))
return ResultCode::Invalid;

const std::string host_path = BuildFilename(path);
const std::string host_path = BuildFilename(path).host_path;
const auto split_path = SplitPathAndBasename(path);

FstEntry* parent = GetFstEntryForPath(split_path.parent);
Expand Down Expand Up @@ -491,8 +506,10 @@ ResultCode HostFileSystem::Rename(Uid uid, Gid gid, const std::string& old_path,
return ResultCode::InUse;
}

const std::string host_old_path = BuildFilename(old_path);
const std::string host_new_path = BuildFilename(new_path);
const auto host_old_info = BuildFilename(old_path);
const auto host_new_info = BuildFilename(new_path);
const std::string& host_old_path = host_old_info.host_path;
const std::string& host_new_path = host_new_info.host_path;

// If there is already something of the same type at the new path, delete it.
if (File::Exists(host_new_path))
Expand All @@ -509,8 +526,27 @@ ResultCode HostFileSystem::Rename(Uid uid, Gid gid, const std::string& old_path,

if (!File::Rename(host_old_path, host_new_path))
{
ERROR_LOG_FMT(IOS_FS, "Rename {} to {} - failed", host_old_path, host_new_path);
return ResultCode::NotFound;
if (host_old_info.is_redirect || host_new_info.is_redirect)
{
// If either path is a redirect, the source and target may be on a different partition or
// device, so a simple rename may not work. Fall back to Copy & Delete and see if that works.
if (!File::Copy(host_old_path, host_new_path))
{
ERROR_LOG_FMT(IOS_FS, "Copying {} to {} in Rename fallback failed", host_old_path,
host_new_path);
return ResultCode::NotFound;
}
if (!File::Delete(host_old_path))
{
ERROR_LOG_FMT(IOS_FS, "Deleting {} in Rename fallback failed", host_old_path);
return ResultCode::Invalid;
}
}
else
{
ERROR_LOG_FMT(IOS_FS, "Rename {} to {} - failed", host_old_path, host_new_path);
return ResultCode::NotFound;
}
}

// Finally, remove the child from the old parent and move it to the new parent.
Expand Down Expand Up @@ -544,7 +580,7 @@ Result<std::vector<std::string>> HostFileSystem::ReadDirectory(Uid uid, Gid gid,
if (entry->data.is_file)
return ResultCode::Invalid;

const std::string host_path = BuildFilename(path);
const std::string host_path = BuildFilename(path).host_path;
File::FSTEntry host_entry = File::ScanDirectoryTree(host_path, false);
for (File::FSTEntry& child : host_entry.children)
{
Expand Down Expand Up @@ -612,7 +648,7 @@ Result<Metadata> HostFileSystem::GetMetadata(Uid uid, Gid gid, const std::string
return ResultCode::NotFound;

Metadata metadata = entry->data;
metadata.size = File::GetSize(BuildFilename(path));
metadata.size = File::GetSize(BuildFilename(path).host_path);
return metadata;
}

Expand All @@ -631,7 +667,7 @@ ResultCode HostFileSystem::SetMetadata(Uid caller_uid, const std::string& path,
if (caller_uid != 0 && uid != entry->data.uid)
return ResultCode::AccessDenied;

const bool is_empty = File::GetSize(BuildFilename(path)) == 0;
const bool is_empty = File::GetSize(BuildFilename(path).host_path) == 0;
if (entry->data.uid != uid && entry->data.is_file && !is_empty)
return ResultCode::FileNotEmpty;

Expand Down Expand Up @@ -667,7 +703,7 @@ Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_
return ResultCode::Invalid;

DirectoryStats stats{};
std::string path(BuildFilename(wii_path));
std::string path(BuildFilename(wii_path).host_path);
if (File::IsDirectory(path))
{
File::FSTEntry parent_dir = File::ScanDirectoryTree(path, true);
Expand All @@ -685,4 +721,8 @@ Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_
return stats;
}

void HostFileSystem::SetNandRedirects(std::vector<NandRedirect> nand_redirects)
{
m_nand_redirects = std::move(nand_redirects);
}
} // namespace IOS::HLE::FS
14 changes: 12 additions & 2 deletions Source/Core/Core/IOS/FS/HostBackend/FS.h
Expand Up @@ -22,7 +22,7 @@ namespace IOS::HLE::FS
class HostFileSystem final : public FileSystem
{
public:
HostFileSystem(const std::string& root_path);
HostFileSystem(const std::string& root_path, std::vector<NandRedirect> nand_redirects = {});
~HostFileSystem();

void DoState(PointerWrap& p) override;
Expand Down Expand Up @@ -56,6 +56,8 @@ class HostFileSystem final : public FileSystem
Result<NandStats> GetNandStats() override;
Result<DirectoryStats> GetDirectoryStats(const std::string& path) override;

void SetNandRedirects(std::vector<NandRedirect> nand_redirects) override;

private:
struct FstEntry
{
Expand Down Expand Up @@ -83,7 +85,12 @@ class HostFileSystem final : public FileSystem
Handle* GetHandleFromFd(Fd fd);
Fd ConvertHandleToFd(const Handle* handle) const;

std::string BuildFilename(const std::string& wii_path) const;
struct HostFilename
{
std::string host_path;
bool is_redirect;
};
HostFilename BuildFilename(const std::string& wii_path) const;
std::shared_ptr<File::IOFile> OpenHostFile(const std::string& host_path);

ResultCode CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path,
Expand Down Expand Up @@ -112,6 +119,9 @@ class HostFileSystem final : public FileSystem
std::string m_root_path;
std::map<std::string, std::weak_ptr<File::IOFile>> m_open_files;
std::array<Handle, 16> m_handles{};

FstEntry m_redirect_fst{};
std::vector<NandRedirect> m_nand_redirects;
};

} // namespace IOS::HLE::FS
2 changes: 1 addition & 1 deletion Source/Core/Core/IOS/FS/HostBackend/File.cpp
Expand Up @@ -80,7 +80,7 @@ Result<FileHandle> HostFileSystem::OpenFile(Uid, Gid, const std::string& path, M
if (!handle)
return ResultCode::NoFreeHandle;

const std::string host_path = BuildFilename(path);
const std::string host_path = BuildFilename(path).host_path;
if (!File::IsFile(host_path))
{
*handle = Handle{};
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/Core/IOS/IOS.cpp
Expand Up @@ -498,7 +498,7 @@ void Kernel::AddDevice(std::unique_ptr<Device> device)

void Kernel::AddCoreDevices()
{
m_fs = FS::MakeFileSystem();
m_fs = FS::MakeFileSystem(IOS::HLE::FS::Location::Session, Core::GetActiveNandRedirects());
ASSERT(m_fs);

std::lock_guard lock(m_device_map_mutex);
Expand Down
43 changes: 35 additions & 8 deletions Source/Core/Core/WiiRoot.cpp
Expand Up @@ -4,6 +4,7 @@
#include "Core/WiiRoot.h"

#include <cinttypes>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -34,6 +35,12 @@ namespace FS = IOS::HLE::FS;

static std::string s_temp_wii_root;
static bool s_wii_root_initialized = false;
static std::vector<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)
{
Expand Down Expand Up @@ -202,6 +209,7 @@ void InitializeWiiRoot(bool use_temporary)
File::SetUserPath(D_SESSION_WIIROOT_IDX, File::GetUserPath(D_WIIROOT_IDX));
}

s_nand_redirects.clear();
s_wii_root_initialized = true;
}

Expand All @@ -213,6 +221,7 @@ void ShutdownWiiRoot()
s_temp_wii_root.clear();
}

s_nand_redirects.clear();
s_wii_root_initialized = false;
}

Expand Down Expand Up @@ -288,7 +297,8 @@ static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_sou
return true;
}

void InitializeWiiFileSystemContents()
void InitializeWiiFileSystemContents(
std::optional<DiscIO::Riivolution::SavegameRedirect> save_redirect)
{
const auto fs = IOS::HLE::GetIOS()->GetFS();

Expand All @@ -299,14 +309,31 @@ void InitializeWiiFileSystemContents()
if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND");

if (!WiiRootIsTemporary())
return;

// Generate a SYSCONF with default settings for the temporary Wii NAND.
SysConf sysconf{fs};
sysconf.Save();
if (WiiRootIsTemporary())
{
// Generate a SYSCONF with default settings for the temporary Wii NAND.
SysConf sysconf{fs};
sysconf.Save();

InitializeDeterministicWiiSaves(fs.get());
InitializeDeterministicWiiSaves(fs.get());
}
else if (save_redirect)
{
const u64 title_id = SConfig::GetInstance().GetTitleID();
std::string source_path = Common::GetTitleDataPath(title_id);
if (!File::IsDirectory(save_redirect->m_target_path))
{
File::CreateFullPath(save_redirect->m_target_path + "/");
if (save_redirect->m_clone)
{
File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_CONFIGURED_ROOT),
save_redirect->m_target_path);
}
}
s_nand_redirects.emplace_back(IOS::HLE::FS::NandRedirect{
std::move(source_path), std::move(save_redirect->m_target_path)});
fs->SetNandRedirects(s_nand_redirects);
}
}

void CleanUpWiiFileSystemContents()
Expand Down
15 changes: 14 additions & 1 deletion Source/Core/Core/WiiRoot.h
Expand Up @@ -3,6 +3,16 @@

#pragma once

#include <optional>
#include <vector>

#include "DiscIO/RiivolutionPatcher.h"

namespace IOS::HLE::FS
{
struct NandRedirect;
}

namespace Core
{
enum class RestoreReason
Expand All @@ -21,6 +31,9 @@ void BackupWiiSettings();
void RestoreWiiSettings(RestoreReason reason);

// Initialize or clean up the filesystem contents.
void InitializeWiiFileSystemContents();
void InitializeWiiFileSystemContents(
std::optional<DiscIO::Riivolution::SavegameRedirect> save_redirect);
void CleanUpWiiFileSystemContents();

const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects();
} // namespace Core
6 changes: 6 additions & 0 deletions Source/Core/DiscIO/CMakeLists.txt
Expand Up @@ -23,11 +23,17 @@ add_library(discio
FileSystemGCWii.h
Filesystem.cpp
Filesystem.h
GameModDescriptor.cpp
GameModDescriptor.h
LaggedFibonacciGenerator.cpp
LaggedFibonacciGenerator.h
MultithreadedCompressor.h
NANDImporter.cpp
NANDImporter.h
RiivolutionParser.cpp
RiivolutionParser.h
RiivolutionPatcher.cpp
RiivolutionPatcher.h
ScrubbedBlob.cpp
ScrubbedBlob.h
TGCBlob.cpp
Expand Down
609 changes: 469 additions & 140 deletions Source/Core/DiscIO/DirectoryBlob.cpp

Large diffs are not rendered by default.

166 changes: 143 additions & 23 deletions Source/Core/DiscIO/DirectoryBlob.h
Expand Up @@ -5,6 +5,7 @@

#include <array>
#include <cstddef>
#include <functional>
#include <map>
#include <memory>
#include <optional>
Expand All @@ -16,6 +17,7 @@
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "DiscIO/Blob.h"
#include "DiscIO/Volume.h"
#include "DiscIO/WiiEncryptionCache.h"

namespace File
Expand All @@ -29,22 +31,108 @@ namespace DiscIO
enum class PartitionType : u32;

class DirectoryBlobReader;
class VolumeDisc;

// Returns true if the path is inside a DirectoryBlob and doesn't represent the DirectoryBlob itself
bool ShouldHideFromGameList(const std::string& volume_path);

// Content chunk that is loaded from a file in the host file system.
struct ContentFile
{
// Path where the file can be found.
std::string m_filename;

// Offset from the start of the file where the first byte of this content chunk is.
u64 m_offset;
};

// Content chunk that loads data from a DirectoryBlobReader.
// Intented for representing a partition within a disc.
struct ContentPartition
{
// The reader to read data from.
DirectoryBlobReader* m_reader;

// Offset from the start of the partition for the first byte represented by this chunk.
u64 m_offset;

// The value passed as partition_data_offset to EncryptPartitionData().
u64 m_partition_data_offset;
};

// Content chunk that loads data from a Volume.
struct ContentVolume
{
// Offset from the start of the volume for the first byte represented by this chunk.
u64 m_offset;

// The volume to read data from.
const Volume* m_volume;

// The partition passed to the Volume's Read() method.
Partition m_partition;
};

// Content chunk representing a run of identical bytes.
// Useful for padding between chunks within a file.
struct ContentFixedByte
{
u8 m_byte;
};

using ContentSource = std::variant<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
{
public:
using ContentSource =
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);
DiscContent(u64 offset, u64 size, ContentSource source);

// Provided because it's convenient when searching for DiscContent in an std::set
explicit DiscContent(u64 offset);
Expand All @@ -62,22 +150,25 @@ class DiscContent
bool operator>=(const DiscContent& other) const { return !(*this < other); }

private:
// Position of this content chunk within its parent DiscContentContainer.
u64 m_offset;

// Number of bytes this content chunk takes up.
u64 m_size = 0;

// Where and how to find the data for this content chunk.
ContentSource m_content_source;
};

class DiscContentContainer
{
public:
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()));
}
void Add(u64 offset, u64 size, const std::string& path);
void Add(u64 offset, u64 size, const u8* data);
void Add(u64 offset, u64 size, DirectoryBlobReader* blob);
void Add(u64 offset, u64 size, ContentSource source);
u64 CheckSizeAndAdd(u64 offset, const std::string& path);
u64 CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path);

Expand All @@ -92,6 +183,10 @@ class DirectoryBlobPartition
public:
DirectoryBlobPartition() = default;
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
DirectoryBlobPartition(const DirectoryBlobPartition&) = delete;
Expand All @@ -104,27 +199,38 @@ class DirectoryBlobPartition
const std::string& GetRootDirectory() const { return m_root_directory; }
const std::vector<u8>& GetHeader() const { return m_disc_header; }
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; }
void SetKey(std::array<u8, VolumeWii::AES_KEY_SIZE> key) { m_key = key; }

private:
void SetDiscHeaderAndDiscType(std::optional<bool> is_wii);
void SetBI2();
void SetDiscHeaderFromFile(const std::string& boot_bin_path);
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
u64 SetApploader();
u64 SetApploaderFromFile(const std::string& path);
u64 SetApploader(std::vector<u8> apploader, const std::string& log_path);
// Returns FST address
u64 SetDOL(u64 dol_address);
u64 SetDOLFromFile(const std::string& path, u64 dol_address);
u64 SetDOL(FSTBuilderNode dol_node, u64 dol_address);

void BuildFST(u64 fst_address);
void BuildFSTFromFolder(const std::string& fst_root_path, u64 fst_address);
void BuildFST(std::vector<FSTBuilderNode> root_nodes, u64 fst_address);

// FST creation
void WriteEntryData(u32* entry_offset, u8 type, u32 name_offset, u64 data_offset, u64 length,
u32 address_shift);
void WriteEntryName(u32* name_offset, const std::string& name, u64 name_table_offset);
void WriteDirectory(const File::FSTEntry& parent_entry, u32* fst_offset, u32* name_offset,
u64* data_offset, u32 parent_entry_index, u64 name_table_offset);
void WriteDirectory(std::vector<FSTBuilderNode>* parent_entries, u32* fst_offset,
u32* name_offset, u64* data_offset, u32 parent_entry_index,
u64 name_table_offset);

DiscContentContainer m_contents;
std::vector<u8> m_disc_header;
Expand All @@ -140,6 +246,8 @@ class DirectoryBlobPartition
u32 m_address_shift = 0;

u64 m_data_size = 0;

std::optional<DiscIO::Partition> m_wrapped_partition = std::nullopt;
};

class DirectoryBlobReader : public BlobReader
Expand All @@ -148,6 +256,10 @@ class DirectoryBlobReader : public BlobReader

public:
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
DirectoryBlobReader(const DirectoryBlobReader&) = delete;
Expand Down Expand Up @@ -183,15 +295,21 @@ class DirectoryBlobReader : public BlobReader

explicit DirectoryBlobReader(const std::string& game_partition_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;

bool EncryptPartitionData(u64 offset, u64 size, u8* buffer, u64 partition_data_offset,
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,
const std::string& game_partition_root);
void SetWiiRegionData(const std::string& game_partition_root);
std::vector<u8> header_bin);
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 SetPartitionHeader(DirectoryBlobPartition* partition, u64 partition_address);

Expand All @@ -209,9 +327,11 @@ class DirectoryBlobReader : public BlobReader
std::vector<u8> m_disc_header_nonpartition;
std::vector<u8> m_partition_table;
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;

std::unique_ptr<DiscIO::VolumeDisc> m_wrapped_volume;
};

} // namespace DiscIO
147 changes: 147 additions & 0 deletions Source/Core/DiscIO/GameModDescriptor.cpp
@@ -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
46 changes: 46 additions & 0 deletions Source/Core/DiscIO/GameModDescriptor.h
@@ -0,0 +1,46 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <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
486 changes: 486 additions & 0 deletions Source/Core/DiscIO/RiivolutionParser.cpp

Large diffs are not rendered by default.

231 changes: 231 additions & 0 deletions Source/Core/DiscIO/RiivolutionParser.h
@@ -0,0 +1,231 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <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
654 changes: 654 additions & 0 deletions Source/Core/DiscIO/RiivolutionPatcher.cpp

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions Source/Core/DiscIO/RiivolutionPatcher.h
@@ -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
40 changes: 27 additions & 13 deletions Source/Core/DiscIO/Volume.cpp
Expand Up @@ -85,8 +85,11 @@ std::map<Language, std::string> Volume::ReadWiiNames(const std::vector<char16_t>
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)
return std::make_unique<VolumeWii>(std::move(reader));

Expand All @@ -97,14 +100,21 @@ static std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader>& reade
return nullptr;
}

std::unique_ptr<VolumeDisc> CreateDisc(std::unique_ptr<BlobReader> reader)
{
return TryCreateDisc(reader);
}

std::unique_ptr<VolumeDisc> CreateDisc(const std::string& path)
{
std::unique_ptr<BlobReader> reader(CreateBlobReader(path));
return reader ? CreateDisc(reader) : nullptr;
return CreateDisc(CreateBlobReader(path));
}

static std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader>& reader)
static std::unique_ptr<VolumeWAD> TryCreateWAD(std::unique_ptr<BlobReader>& reader)
{
if (!reader)
return nullptr;

// Check for WAD
// 0x206962 for boot2 wads
const std::optional<u32> wad_magic = reader->ReadSwapped<u32>(0x02);
Expand All @@ -115,27 +125,31 @@ static std::unique_ptr<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader>& reader)
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 reader ? CreateWAD(reader) : nullptr;
return TryCreateWAD(reader);
}

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));
if (reader == nullptr)
return nullptr;
return CreateWAD(CreateBlobReader(path));
}

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)
return disc;

std::unique_ptr<VolumeWAD> wad = CreateWAD(reader);
std::unique_ptr<VolumeWAD> wad = TryCreateWAD(reader);
if (wad)
return wad;

return nullptr;
}

std::unique_ptr<Volume> CreateVolume(const std::string& path)
{
return CreateVolume(CreateBlobReader(path));
}
} // namespace DiscIO
3 changes: 3 additions & 0 deletions Source/Core/DiscIO/Volume.h
Expand Up @@ -182,8 +182,11 @@ class Volume
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<VolumeWAD> CreateWAD(std::unique_ptr<BlobReader> reader);
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);

} // namespace DiscIO
6 changes: 6 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -430,9 +430,12 @@
<ClInclude Include="DiscIO\FileBlob.h" />
<ClInclude Include="DiscIO\Filesystem.h" />
<ClInclude Include="DiscIO\FileSystemGCWii.h" />
<ClInclude Include="DiscIO\GameModDescriptor.h" />
<ClInclude Include="DiscIO\LaggedFibonacciGenerator.h" />
<ClInclude Include="DiscIO\MultithreadedCompressor.h" />
<ClInclude Include="DiscIO\NANDImporter.h" />
<ClInclude Include="DiscIO\RiivolutionParser.h" />
<ClInclude Include="DiscIO\RiivolutionPatcher.h" />
<ClInclude Include="DiscIO\ScrubbedBlob.h" />
<ClInclude Include="DiscIO\TGCBlob.h" />
<ClInclude Include="DiscIO\Volume.h" />
Expand Down Expand Up @@ -1017,8 +1020,11 @@
<ClCompile Include="DiscIO\FileBlob.cpp" />
<ClCompile Include="DiscIO\Filesystem.cpp" />
<ClCompile Include="DiscIO\FileSystemGCWii.cpp" />
<ClCompile Include="DiscIO\GameModDescriptor.cpp" />
<ClCompile Include="DiscIO\LaggedFibonacciGenerator.cpp" />
<ClCompile Include="DiscIO\NANDImporter.cpp" />
<ClCompile Include="DiscIO\RiivolutionParser.cpp" />
<ClCompile Include="DiscIO\RiivolutionPatcher.cpp" />
<ClCompile Include="DiscIO\ScrubbedBlob.cpp" />
<ClCompile Include="DiscIO\TGCBlob.cpp" />
<ClCompile Include="DiscIO\Volume.cpp" />
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/CMakeLists.txt
Expand Up @@ -291,6 +291,8 @@ add_executable(dolphin-emu
QtUtils/AspectRatioWidget.h
ResourcePackManager.cpp
ResourcePackManager.h
RiivolutionBootWidget.cpp
RiivolutionBootWidget.h
Settings/AdvancedPane.cpp
Settings/AdvancedPane.h
Settings/AudioPane.cpp
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/DolphinQt.vcxproj
Expand Up @@ -179,6 +179,7 @@
<ClCompile Include="RenderWidget.cpp" />
<ClCompile Include="ResourcePackManager.cpp" />
<ClCompile Include="Resources.cpp" />
<ClCompile Include="RiivolutionBootWidget.cpp" />
<ClCompile Include="SearchBar.cpp" />
<ClCompile Include="Settings.cpp" />
<ClCompile Include="Settings\AdvancedPane.cpp" />
Expand Down Expand Up @@ -351,6 +352,7 @@
<QtMoc Include="QtUtils\UTF8CodePointCountValidator.h" />
<QtMoc Include="QtUtils\WindowActivationEventFilter.h" />
<QtMoc Include="RenderWidget.h" />
<QtMoc Include="RiivolutionBootWidget.h" />
<QtMoc Include="SearchBar.h" />
<QtMoc Include="Settings.h" />
<QtMoc Include="Settings\AdvancedPane.h" />
Expand Down
14 changes: 14 additions & 0 deletions Source/Core/DolphinQt/GameList/GameList.cpp
Expand Up @@ -353,6 +353,11 @@ void GameList::ShowContextMenu(const QPoint&)

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);

if (game->ShouldAllowConversion())
Expand Down Expand Up @@ -587,6 +592,15 @@ void GameList::UninstallWAD()
result_dialog.exec();
}

void GameList::StartWithRiivolution()
{
const auto game = GetSelectedGame();
if (!game)
return;

emit OnStartWithRiivolution(*game);
}

void GameList::SetDefaultISO()
{
const auto game = GetSelectedGame();
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/DolphinQt/GameList/GameList.h
Expand Up @@ -50,6 +50,7 @@ class GameList final : public QStackedWidget

signals:
void GameSelected();
void OnStartWithRiivolution(const UICommon::GameFile& game);
void NetPlayHost(const UICommon::GameFile& game);
void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file);
void OpenGeneralSettings();
Expand All @@ -62,6 +63,7 @@ class GameList final : public QStackedWidget
void OpenWiiSaveFolder();
void OpenGCSaveFolder();
void OpenWiki();
void StartWithRiivolution();
void SetDefaultISO();
void DeleteFile();
#ifdef _WIN32
Expand Down
29 changes: 29 additions & 0 deletions Source/Core/DolphinQt/MainWindow.cpp
Expand Up @@ -58,7 +58,9 @@
#include "Core/State.h"
#include "Core/WiiUtils.h"

#include "DiscIO/DirectoryBlob.h"
#include "DiscIO/NANDImporter.h"
#include "DiscIO/RiivolutionPatcher.h"

#include "DolphinQt/AboutDialog.h"
#include "DolphinQt/CheatsManager.h"
Expand Down Expand Up @@ -100,6 +102,7 @@
#include "DolphinQt/RenderWidget.h"
#include "DolphinQt/ResourcePackManager.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/RiivolutionBootWidget.h"
#include "DolphinQt/SearchBar.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/TAS/GCTASInputWindow.h"
Expand Down Expand Up @@ -643,6 +646,8 @@ void MainWindow::ConnectGameList()
{
connect(m_game_list, &GameList::GameSelected, this, [this]() { Play(); });
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);
}
Expand Down Expand Up @@ -1807,6 +1812,30 @@ void MainWindow::ShowCheatsManager()
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()
{
if (!Settings::Instance().IsBatchModeEnabled())
Expand Down
1 change: 1 addition & 0 deletions Source/Core/DolphinQt/MainWindow.h
Expand Up @@ -157,6 +157,7 @@ class MainWindow final : public QMainWindow
void ShowMemcardManager();
void ShowResourcePackManager();
void ShowCheatsManager();
void ShowRiivolutionBootWidget(const UICommon::GameFile& game);

void NetPlayInit();
bool NetPlayJoin();
Expand Down
281 changes: 281 additions & 0 deletions Source/Core/DolphinQt/RiivolutionBootWidget.cpp
@@ -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();
}
53 changes: 53 additions & 0 deletions Source/Core/DolphinQt/RiivolutionBootWidget.h
@@ -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;
};
1 change: 1 addition & 0 deletions Source/Core/UICommon/UICommon.cpp
Expand Up @@ -67,6 +67,7 @@ static void CreateLoadPath(const std::string& path)
if (!path.empty())
File::SetUserPath(D_LOAD_IDX, path + '/');
File::CreateFullPath(File::GetUserPath(D_HIRESTEXTURES_IDX));
File::CreateFullPath(File::GetUserPath(D_RIIVOLUTION_IDX));
}

static void CreateResourcePackPath(const std::string& path)
Expand Down