mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 09:11:52 -04:00
368 lines
14 KiB
C++
368 lines
14 KiB
C++
// Riivolution overlay loading (host-IO shim). See riivolution.h.
|
|
//
|
|
// Ported behavior from Dolphin Emulator's DiscIO/RiivolutionParser and
|
|
// RiivolutionPatcher (https://github.com/dolphin-emu/dolphin).
|
|
// Copyright 2021 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "hle/storage/riivolution.h"
|
|
|
|
#include "hle/riivolution_contract.h"
|
|
#include "hle/runtime_parse_helpers.h"
|
|
#include "memory.h"
|
|
#include "nand_path.h"
|
|
#include "recomp_mod_loader.h"
|
|
#include "runtime_config.h"
|
|
#include "runtime_log.h"
|
|
#include "runtime_product.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <system_error>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
struct RiivoState {
|
|
std::vector<RuntimeRiivolution::Overlay> overlays;
|
|
std::optional<RuntimeRiivolution::SaveRedirect> saveRedirect;
|
|
};
|
|
|
|
std::once_flag g_riivoOnce;
|
|
RiivoState g_riivoState;
|
|
|
|
std::optional<std::string> RiivoReadFile(const fs::path& path) {
|
|
std::ifstream stream(path, std::ios::binary);
|
|
if (!stream.is_open()) {
|
|
return std::nullopt;
|
|
}
|
|
std::ostringstream buffer;
|
|
buffer << stream.rdbuf();
|
|
return buffer.str();
|
|
}
|
|
|
|
// Six-character game ID ("RMCP01") from guest low memory, with the same RMCP
|
|
// fallback the rest of the storage HLE uses for boots that have not written
|
|
// the disc header yet.
|
|
std::string RiivoGameId() {
|
|
const uint32_t code = RuntimeHle::CurrentGameCode(0x524D4350u); // "RMCP"
|
|
std::string id(6, '\0');
|
|
id[0] = static_cast<char>((code >> 24) & 0xffu);
|
|
id[1] = static_cast<char>((code >> 16) & 0xffu);
|
|
id[2] = static_cast<char>((code >> 8) & 0xffu);
|
|
id[3] = static_cast<char>(code & 0xffu);
|
|
|
|
uint16_t maker = 0;
|
|
if (Memory::Contains(0x80000004u, 2u)) {
|
|
maker = Memory::Read16(0x80000004u);
|
|
}
|
|
const char makerHi = static_cast<char>((maker >> 8) & 0xffu);
|
|
const char makerLo = static_cast<char>(maker & 0xffu);
|
|
if (std::isalnum(static_cast<unsigned char>(makerHi)) &&
|
|
std::isalnum(static_cast<unsigned char>(makerLo))) {
|
|
id[4] = makerHi;
|
|
id[5] = makerLo;
|
|
} else {
|
|
id[4] = '0';
|
|
id[5] = '1';
|
|
}
|
|
return id;
|
|
}
|
|
|
|
// Every narrow path string here is UTF-8, including the ones the XML halves of
|
|
// resolved paths are concatenated with.
|
|
using RuntimeConfigFile::PathFromUtf8;
|
|
using RuntimeConfigFile::PathToUtf8;
|
|
|
|
std::string RiivoGenericText(const fs::path& path) {
|
|
const std::u8string text = path.generic_u8string();
|
|
return std::string(text.begin(), text.end());
|
|
}
|
|
|
|
std::string RiivoComparablePath(const fs::path& path) {
|
|
std::string text = RiivoGenericText(path.lexically_normal());
|
|
#ifdef _WIN32
|
|
RuntimeHle::LowerInPlace(text);
|
|
#endif
|
|
return text;
|
|
}
|
|
|
|
void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path root,
|
|
const char* source) {
|
|
std::error_code ec;
|
|
if (!fs::is_directory(root, ec)) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "rejected overlay root (" << (source ? source : "unknown")
|
|
<< "): " << PathToUtf8(root) << " is not a reachable directory" << std::endl;
|
|
return;
|
|
}
|
|
|
|
fs::path normalized = fs::weakly_canonical(root, ec);
|
|
if (ec) {
|
|
normalized = root.lexically_normal();
|
|
}
|
|
|
|
const std::string comparable = RiivoComparablePath(normalized);
|
|
const auto duplicate =
|
|
std::find_if(overlays.begin(), overlays.end(), [&](const RuntimeRiivolution::Overlay& overlay) {
|
|
return RiivoComparablePath(overlay.root) == comparable;
|
|
});
|
|
if (duplicate != overlays.end()) {
|
|
return;
|
|
}
|
|
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown")
|
|
<< "): " << PathToUtf8(normalized) << std::endl;
|
|
overlays.push_back({std::move(normalized), std::nullopt});
|
|
}
|
|
|
|
std::vector<RuntimeRiivolution::Overlay> RiivoDiscoverRoots() {
|
|
std::vector<RuntimeRiivolution::Overlay> overlays;
|
|
|
|
// Discovery order is precedence order: DVDInit applies the roots in
|
|
// reverse, so an explicitly configured root (command line first, then
|
|
// Config.toml) always outranks the mod manifest.
|
|
for (const auto& root : RuntimeConfigFile::OverlayRoots()) {
|
|
// A relative overlay root resolves against the config file that named
|
|
// it, never against whatever working directory the process happened
|
|
// to be started in.
|
|
RiivoAddRoot(overlays, RuntimeNandPath::ResolveConfiguredPath(root),
|
|
"command line or Config.toml");
|
|
}
|
|
|
|
// The one canonical Retro Rewind installation setup recorded. Only the
|
|
// Retro Rewind product applies it: overlaying the base product with the
|
|
// pack's menu archives made "base game" boot as a half-Retro-Rewind build.
|
|
if (const auto retroRewindRoot =
|
|
RuntimeProduct::IsRetroRewind() ? RuntimeConfigFile::RetroRewindRoot() : std::string{};
|
|
!retroRewindRoot.empty()) {
|
|
RiivoAddRoot(overlays, RuntimeNandPath::ResolveConfiguredPath(retroRewindRoot),
|
|
"canonical Retro Rewind installation");
|
|
}
|
|
|
|
for (const auto& root : RecompMod::DvdOverlayRoots()) {
|
|
RiivoAddRoot(overlays, root, "recomp mod manifest");
|
|
}
|
|
|
|
return overlays;
|
|
}
|
|
|
|
// The Riivolution XMLs describing an overlay root, plus the SD-root directory
|
|
// that absolute external paths resolve against.
|
|
struct RiivoXmlSet {
|
|
fs::path sdRoot;
|
|
std::vector<fs::path> xmlFiles;
|
|
};
|
|
|
|
std::optional<RiivoXmlSet> RiivoFindXmls(const fs::path& overlayRoot) {
|
|
std::error_code ec;
|
|
|
|
// Distribution-pinned XML from recomp.yml: a path relative to the pack
|
|
// folder. The pack folder sits inside the virtual SD root (e.g.
|
|
// <sd>/RetroRewind6), so externals resolve against the root's parent.
|
|
const std::string& configured = RecompMod::RiivolutionXml();
|
|
if (!configured.empty()) {
|
|
const fs::path configuredXml = overlayRoot / PathFromUtf8(configured);
|
|
if (fs::is_regular_file(configuredXml, ec)) {
|
|
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
|
|
}
|
|
}
|
|
|
|
// Dolphin-style discovery: the overlay root is itself a virtual SD root
|
|
// carrying <root>/riivolution/*.xml.
|
|
const fs::path xmlDirectory = overlayRoot / "riivolution";
|
|
if (fs::is_directory(xmlDirectory, ec)) {
|
|
std::vector<fs::path> xmlFiles;
|
|
for (const auto& entry : fs::directory_iterator(xmlDirectory, ec)) {
|
|
if (ec) {
|
|
break;
|
|
}
|
|
if (entry.is_regular_file(ec) && entry.path().extension() == ".xml") {
|
|
xmlFiles.push_back(entry.path());
|
|
}
|
|
}
|
|
if (!xmlFiles.empty()) {
|
|
// Sort so the same folder always produces the same disc.
|
|
std::sort(xmlFiles.begin(), xmlFiles.end());
|
|
return RiivoXmlSet{overlayRoot, std::move(xmlFiles)};
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<RiivolutionContract::OptionSelection> RiivoManifestSelections() {
|
|
std::vector<RiivolutionContract::OptionSelection> selections;
|
|
for (const auto& selection : RecompMod::RiivolutionOptionSelections()) {
|
|
selections.push_back({selection.section, selection.option, selection.choice});
|
|
}
|
|
return selections;
|
|
}
|
|
|
|
void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::string& sdRootGeneric,
|
|
const std::string& xmlDirGeneric, RuntimeRiivolution::PatchSet& set) {
|
|
const std::string patchRoot =
|
|
RiivolutionContract::ResolvePatchRoot(sdRootGeneric, xmlDirGeneric, patch.root);
|
|
std::error_code ec;
|
|
|
|
// Dolphin applies each patch's file patches first, then its folder
|
|
// patches; with last-registration-wins that means folders shadow files
|
|
// within one patch, and later patches shadow earlier ones.
|
|
for (const auto& file : patch.filePatches) {
|
|
if (file.disc.empty()) {
|
|
++set.skippedExternals;
|
|
continue;
|
|
}
|
|
const auto resolved =
|
|
RiivolutionContract::MakeAbsoluteFromRelative(sdRootGeneric, patchRoot, file.external);
|
|
if (!resolved) {
|
|
++set.skippedExternals;
|
|
continue;
|
|
}
|
|
const fs::path hostFile = PathFromUtf8(*resolved);
|
|
if (!fs::is_regular_file(hostFile, ec)) {
|
|
++set.skippedExternals;
|
|
continue;
|
|
}
|
|
set.mappings.push_back(
|
|
{RuntimeRiivolution::Mapping::Kind::File, file.disc, hostFile, true, file.create});
|
|
}
|
|
|
|
for (const auto& folder : patch.folderPatches) {
|
|
const auto resolved =
|
|
RiivolutionContract::MakeAbsoluteFromRelative(sdRootGeneric, patchRoot, folder.external);
|
|
if (!resolved) {
|
|
++set.skippedExternals;
|
|
continue;
|
|
}
|
|
const fs::path hostFolder = PathFromUtf8(*resolved);
|
|
if (!fs::is_directory(hostFolder, ec)) {
|
|
++set.skippedExternals;
|
|
continue;
|
|
}
|
|
const auto kind = folder.disc.empty() ? RuntimeRiivolution::Mapping::Kind::FolderByName
|
|
: RuntimeRiivolution::Mapping::Kind::Folder;
|
|
set.mappings.push_back({kind, folder.disc, hostFolder, folder.recursive, folder.create});
|
|
}
|
|
}
|
|
|
|
std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& overlayRoot,
|
|
RiivoState& state) {
|
|
const auto xmlSet = RiivoFindXmls(overlayRoot);
|
|
if (!xmlSet) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const std::string gameId = RiivoGameId();
|
|
const std::string sdRootGeneric = RiivoGenericText(xmlSet->sdRoot);
|
|
const auto manifestSelections = RiivoManifestSelections();
|
|
|
|
// Dolphin-compatible remembered choices; a recomp.yml pin overrides them.
|
|
std::optional<RiivolutionContract::Config> config;
|
|
const fs::path configXml =
|
|
xmlSet->sdRoot / "riivolution" / "config" / (gameId.substr(0, 4) + ".xml");
|
|
if (const auto configText = RiivoReadFile(configXml)) {
|
|
config = RiivolutionContract::ParseConfigString(*configText);
|
|
}
|
|
|
|
RuntimeRiivolution::PatchSet set;
|
|
for (const fs::path& xmlFile : xmlSet->xmlFiles) {
|
|
const auto text = RiivoReadFile(xmlFile);
|
|
if (!text) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << PathToUtf8(xmlFile)
|
|
<< std::endl;
|
|
continue;
|
|
}
|
|
|
|
auto disc = RiivolutionContract::ParseString(*text);
|
|
if (!disc) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
|
<< " is not a valid Riivolution XML (version 1 wiidisc); ignoring it"
|
|
<< std::endl;
|
|
continue;
|
|
}
|
|
if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": not valid for " << gameId
|
|
<< ", skipped" << std::endl;
|
|
continue;
|
|
}
|
|
|
|
if (config) {
|
|
RiivolutionContract::ApplyConfigDefaults(*disc, *config);
|
|
}
|
|
RiivolutionContract::ApplySelections(*disc, manifestSelections);
|
|
|
|
const auto activePatches = disc->GeneratePatches(gameId);
|
|
const std::string xmlDirGeneric = RiivoGenericText(xmlFile.parent_path());
|
|
|
|
const size_t before = set.mappings.size();
|
|
for (const auto& patch : activePatches) {
|
|
RiivoCollectMappings(patch, sdRootGeneric, xmlDirGeneric, set);
|
|
}
|
|
|
|
// Highest-priority overlay with a <savegame> wins.
|
|
if (!state.saveRedirect) {
|
|
if (const auto* savegame = RiivolutionContract::FindSavegamePatch(activePatches)) {
|
|
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
|
|
sdRootGeneric, xmlDirGeneric, savegame->external)) {
|
|
state.saveRedirect =
|
|
RuntimeRiivolution::SaveRedirect{PathFromUtf8(*resolvedSave),
|
|
savegame->clone};
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
|
|
<< PathToUtf8(state.saveRedirect->hostDirectory)
|
|
<< (savegame->clone ? " (clone)" : "") << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A pack whose XML parses but activates nothing is the most confusing
|
|
// failure this layer has: the game boots, plays, and quietly shows
|
|
// vanilla content. Always say what happened.
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": " << activePatches.size()
|
|
<< " active patch(es), " << (set.mappings.size() - before) << " mapping(s)"
|
|
<< std::endl;
|
|
if (activePatches.empty()) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
|
<< " has no enabled options for " << gameId
|
|
<< "; check the riivolution option selections (recomp.yml) or "
|
|
<< sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml"
|
|
<< std::endl;
|
|
}
|
|
}
|
|
|
|
if (set.skippedExternals != 0) {
|
|
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(overlayRoot) << ": skipped "
|
|
<< set.skippedExternals << " mapping(s) whose external path does not exist"
|
|
<< std::endl;
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
void RiivoInitialize() {
|
|
g_riivoState.overlays = RiivoDiscoverRoots();
|
|
for (auto& overlay : g_riivoState.overlays) {
|
|
overlay.patches = RiivoLoadPatchSet(overlay.root, g_riivoState);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace RuntimeRiivolution {
|
|
|
|
const std::vector<Overlay>& Overlays() {
|
|
std::call_once(g_riivoOnce, RiivoInitialize);
|
|
return g_riivoState.overlays;
|
|
}
|
|
|
|
const std::optional<SaveRedirect>& GetSaveRedirect() {
|
|
std::call_once(g_riivoOnce, RiivoInitialize);
|
|
return g_riivoState.saveRedirect;
|
|
}
|
|
|
|
} // namespace RuntimeRiivolution
|