mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
f424536d3b
* treat empty mkw save as missing first-run format zero-fills rksys.dat before any real save; a quit before the first save left an all-zero file that read back as corrupt and trapped the user in a delete/recreate loop. read opens now treat an all-zero rksys.dat as absent (a real save always begins with the RKSD0006 header), so the game recreates it from scratch. also ignore native build output. * shorten * I dont really want to change this to be honest. * extra safety --------- Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
483 lines
16 KiB
C++
483 lines
16 KiB
C++
// NAND/ISFS HLE: redirects Wii NAND paths (e.g. /title/00010004/524d4350/data/rksys.dat) to
|
|
// <nand_root>\title\00010004\524d4350\data\rksys.dat on the host.
|
|
|
|
#include "nand_internal.h"
|
|
|
|
#include "isa/big_endian.h"
|
|
#include "hle/storage/riivolution.h"
|
|
#include "runtime_log.h"
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
// Base path for the host Wii NAND directory (resolved at runtime).
|
|
static std::filesystem::path g_dolphinWiiBase;
|
|
static std::once_flag g_dolphinWiiBaseOnce;
|
|
|
|
// ============================================================================
|
|
// Logging
|
|
// ============================================================================
|
|
|
|
static void EmitNandLog(const char* func, const char* fmt, va_list args) {
|
|
char buf[512];
|
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
RT_LOGF(RT_TAG_NAND, "%s: %s\n", func, buf);
|
|
}
|
|
|
|
void LogNandError(const char* func, const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
EmitNandLog(func, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void LogNandWarning(const char* func, const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
EmitNandLog(func, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// ============================================================================
|
|
// File Descriptor Management
|
|
// ============================================================================
|
|
|
|
std::map<int32_t, FileHandle> g_fileHandles;
|
|
static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds
|
|
std::mutex g_fdMutex;
|
|
|
|
int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode) {
|
|
std::lock_guard<std::mutex> lock(g_fdMutex);
|
|
int32_t fd = g_nextFd++;
|
|
g_fileHandles[fd] = {file, path, mode, 0};
|
|
return fd;
|
|
}
|
|
|
|
FileHandle* GetHandle(int32_t fd) {
|
|
auto it = g_fileHandles.find(fd);
|
|
if (it == g_fileHandles.end()) {
|
|
return nullptr;
|
|
}
|
|
return &it->second;
|
|
}
|
|
|
|
void CloseFd(int32_t fd) {
|
|
std::lock_guard<std::mutex> lock(g_fdMutex);
|
|
auto it = g_fileHandles.find(fd);
|
|
if (it != g_fileHandles.end()) {
|
|
if (it->second.file) {
|
|
std::fclose(it->second.file);
|
|
}
|
|
g_fileHandles.erase(it);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Path Translation
|
|
// ============================================================================
|
|
|
|
uint32_t CurrentMkwTitleIdLo() {
|
|
return RuntimeHle::CurrentGameCode(kNandTitleIdLo);
|
|
}
|
|
|
|
std::string CurrentNandDataDir() {
|
|
char path[64];
|
|
std::snprintf(path, sizeof(path), "/title/%08x/%08x/data",
|
|
kNandTitleIdHi, CurrentMkwTitleIdLo());
|
|
return path;
|
|
}
|
|
|
|
const std::filesystem::path& GetNandBasePath() {
|
|
std::call_once(g_dolphinWiiBaseOnce, []() {
|
|
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootPath();
|
|
});
|
|
|
|
return g_dolphinWiiBase;
|
|
}
|
|
|
|
std::string HostPathText(const std::filesystem::path& path) {
|
|
return RuntimeConfigFile::PathToUtf8(path);
|
|
}
|
|
|
|
FILE* NandFopen(const std::filesystem::path& path, const char* mode) {
|
|
#ifdef _WIN32
|
|
const std::wstring wideMode(mode, mode + std::strlen(mode));
|
|
return _wfopen(path.c_str(), wideMode.c_str());
|
|
#else
|
|
return std::fopen(path.c_str(), mode);
|
|
#endif
|
|
}
|
|
|
|
bool NandRemove(const std::filesystem::path& path) {
|
|
std::error_code ec;
|
|
return std::filesystem::remove(path, ec) && !ec;
|
|
}
|
|
|
|
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to) {
|
|
std::error_code ec;
|
|
std::filesystem::rename(from, to, ec);
|
|
return !ec;
|
|
}
|
|
|
|
// Guest paths are absolute and already lexically resolved against the NAND root, so
|
|
// they are appended as relative components instead of replacing the root.
|
|
static std::filesystem::path BuildHostNandPath(const std::string& wiiPathStr) {
|
|
std::filesystem::path hostPath = GetNandBasePath();
|
|
size_t cursor = 0;
|
|
while (cursor < wiiPathStr.size()) {
|
|
const size_t slash = wiiPathStr.find('/', cursor);
|
|
const size_t end = slash == std::string::npos ? wiiPathStr.size() : slash;
|
|
if (end != cursor) {
|
|
hostPath /= wiiPathStr.substr(cursor, end - cursor);
|
|
}
|
|
cursor = end + 1;
|
|
}
|
|
return hostPath;
|
|
}
|
|
|
|
// Collapses "." and ".." components against the NAND root. Without this, guest paths like
|
|
// "/title/../../../../Users/..." escape the NAND root into the real filesystem under the
|
|
// current user's permissions. ".." at the root clamps to the root, matching IOS.
|
|
static std::string LexicallyResolveWiiPath(const std::string& path) {
|
|
std::vector<std::string> components;
|
|
std::string component;
|
|
// path is always absolute here, so a leading empty component is implied.
|
|
for (size_t i = 1; i <= path.size(); ++i) {
|
|
if (i < path.size() && path[i] != '/') {
|
|
component.push_back(path[i]);
|
|
continue;
|
|
}
|
|
if (component == "..") {
|
|
if (!components.empty()) {
|
|
components.pop_back();
|
|
}
|
|
} else if (!component.empty() && component != ".") {
|
|
components.push_back(component);
|
|
}
|
|
component.clear();
|
|
}
|
|
|
|
std::string resolved;
|
|
for (const std::string& part : components) {
|
|
resolved.push_back('/');
|
|
resolved += part;
|
|
}
|
|
return resolved.empty() ? "/" : resolved;
|
|
}
|
|
|
|
static std::string NormalizeAbsoluteWiiPath(const char* wiiPath) {
|
|
if (!wiiPath || wiiPath[0] == '\0') {
|
|
return {};
|
|
}
|
|
|
|
std::string path = wiiPath;
|
|
std::replace(path.begin(), path.end(), '\\', '/');
|
|
if (path[0] != '/') {
|
|
std::string cwd = CurrentNandDataDir();
|
|
if (!cwd.empty() && cwd.back() != '/') {
|
|
cwd.push_back('/');
|
|
}
|
|
path = cwd + path;
|
|
}
|
|
path = LexicallyResolveWiiPath(path);
|
|
while (path.size() > 1 && path.back() == '/') {
|
|
path.pop_back();
|
|
}
|
|
return path;
|
|
}
|
|
|
|
struct RiivolutionSaveRedirect {
|
|
bool enabled = false;
|
|
bool clone = false;
|
|
std::filesystem::path hostDirectory;
|
|
};
|
|
|
|
static std::once_flag g_riivolutionSaveRedirectOnce;
|
|
static RiivolutionSaveRedirect g_riivolutionSaveRedirect;
|
|
|
|
static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
|
|
// The active pack's <savegame> patch, resolved against the virtual SD root
|
|
// exactly like Dolphin resolves it for Riivolution launches. This keeps
|
|
// the recomp's save in the same folder a Dolphin/WheelWizard launch of the
|
|
// pack uses (e.g. .../Riivolution/WheelWizard/riivolution/save/RetroWFC/RMCP).
|
|
std::call_once(g_riivolutionSaveRedirectOnce, []() {
|
|
const auto& redirect = RuntimeRiivolution::GetSaveRedirect();
|
|
if (!redirect) {
|
|
return;
|
|
}
|
|
g_riivolutionSaveRedirect.enabled = true;
|
|
g_riivolutionSaveRedirect.clone = redirect->clone;
|
|
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory;
|
|
// Riivolution creates the redirect folder if it does not exist.
|
|
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
|
|
});
|
|
|
|
return g_riivolutionSaveRedirect;
|
|
}
|
|
|
|
static void CloneRiivolutionSaveIfNeeded(const std::filesystem::path& sourceHostPath,
|
|
const std::filesystem::path& redirectedHostPath,
|
|
const RiivolutionSaveRedirect& redirect) {
|
|
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
|
|
return;
|
|
}
|
|
|
|
CreateParentDirectories(redirectedHostPath);
|
|
|
|
std::error_code ec;
|
|
std::filesystem::copy_file(sourceHostPath, redirectedHostPath,
|
|
std::filesystem::copy_options::skip_existing, ec);
|
|
if (ec) {
|
|
LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s",
|
|
HostPathText(sourceHostPath).c_str(),
|
|
HostPathText(redirectedHostPath).c_str(), ec.message().c_str());
|
|
}
|
|
}
|
|
|
|
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath,
|
|
std::filesystem::path& outHostPath) {
|
|
const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect();
|
|
if (!redirect.enabled) {
|
|
return false;
|
|
}
|
|
|
|
const std::string dataDir = CurrentNandDataDir();
|
|
if (absoluteWiiPath != dataDir &&
|
|
(absoluteWiiPath.size() <= dataDir.size() ||
|
|
absoluteWiiPath.compare(0, dataDir.size(), dataDir) != 0 ||
|
|
absoluteWiiPath[dataDir.size()] != '/')) {
|
|
return false;
|
|
}
|
|
|
|
std::string relative;
|
|
if (absoluteWiiPath.size() > dataDir.size()) {
|
|
relative = absoluteWiiPath.substr(dataDir.size() + 1);
|
|
}
|
|
|
|
std::filesystem::path redirected = redirect.hostDirectory;
|
|
if (!relative.empty()) {
|
|
redirected /= std::filesystem::path(relative);
|
|
}
|
|
|
|
outHostPath = redirected;
|
|
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
|
|
return true;
|
|
}
|
|
|
|
std::filesystem::path TranslateNandPath(const char* wiiPath) {
|
|
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
|
|
if (wiiPathStr.empty()) {
|
|
return {};
|
|
}
|
|
|
|
std::filesystem::path redirectedHostPath;
|
|
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
|
|
return redirectedHostPath;
|
|
}
|
|
|
|
return BuildHostNandPath(wiiPathStr);
|
|
}
|
|
|
|
// ============================================================================
|
|
// U8 archive helper (used to seed FaceLib resources from the ISO)
|
|
// ============================================================================
|
|
|
|
struct U8Node {
|
|
uint32_t typeAndNameOffset;
|
|
uint32_t dataOffset;
|
|
uint32_t size;
|
|
};
|
|
|
|
static bool ExtractFromU8(const std::filesystem::path& archivePath, const char* targetName,
|
|
std::vector<uint8_t>& outData) {
|
|
std::ifstream file(archivePath, std::ios::binary);
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
file.seekg(0, std::ios::end);
|
|
const std::streamsize size = file.tellg();
|
|
if (size < 0x20) {
|
|
return false;
|
|
}
|
|
file.seekg(0, std::ios::beg);
|
|
|
|
std::vector<uint8_t> data(static_cast<size_t>(size));
|
|
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
|
return false;
|
|
}
|
|
|
|
const uint32_t magic = BigEndian::Read32(data.data());
|
|
if (magic != 0x55AA382Du) { // "U\xAA8-"
|
|
return false;
|
|
}
|
|
|
|
const uint32_t rootOffset = BigEndian::Read32(data.data() + 0x04);
|
|
if (rootOffset + 0x0C > data.size()) {
|
|
return false;
|
|
}
|
|
|
|
const U8Node* root = reinterpret_cast<const U8Node*>(data.data() + rootOffset);
|
|
const uint32_t nodeCount = BigEndian::Read32(reinterpret_cast<const uint8_t*>(&root->size));
|
|
const uint32_t nodeTableSize = nodeCount * sizeof(U8Node);
|
|
const uint32_t stringTableOffset = rootOffset + nodeTableSize;
|
|
if (stringTableOffset >= data.size()) {
|
|
return false;
|
|
}
|
|
|
|
struct DirFrame {
|
|
std::string path;
|
|
uint32_t endIndex;
|
|
};
|
|
std::vector<DirFrame> stack;
|
|
stack.push_back({"", nodeCount});
|
|
|
|
for (uint32_t index = 1; index < nodeCount; ++index) {
|
|
while (!stack.empty() && index >= stack.back().endIndex) {
|
|
stack.pop_back();
|
|
}
|
|
if (stack.empty()) {
|
|
break;
|
|
}
|
|
|
|
const U8Node* node = reinterpret_cast<const U8Node*>(data.data() + rootOffset + index * sizeof(U8Node));
|
|
const uint32_t typeAndName = BigEndian::Read32(reinterpret_cast<const uint8_t*>(&node->typeAndNameOffset));
|
|
const uint32_t nameOffset = typeAndName & 0x00FFFFFFu;
|
|
const bool isDir = (typeAndName >> 24) != 0;
|
|
if (stringTableOffset + nameOffset >= data.size()) {
|
|
return false;
|
|
}
|
|
|
|
const char* name = reinterpret_cast<const char*>(data.data() + stringTableOffset + nameOffset);
|
|
const std::string fullPath = stack.back().path + name;
|
|
|
|
if (isDir) {
|
|
const uint32_t endIndex = BigEndian::Read32(reinterpret_cast<const uint8_t*>(&node->size));
|
|
stack.push_back({fullPath + "/", endIndex});
|
|
continue;
|
|
}
|
|
|
|
if (fullPath == targetName || std::strcmp(name, targetName) == 0) {
|
|
const uint32_t fileOffset = BigEndian::Read32(reinterpret_cast<const uint8_t*>(&node->dataOffset));
|
|
const uint32_t fileSize = BigEndian::Read32(reinterpret_cast<const uint8_t*>(&node->size));
|
|
if (fileOffset + fileSize > data.size()) {
|
|
return false;
|
|
}
|
|
outData.assign(data.begin() + fileOffset, data.begin() + fileOffset + fileSize);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// The only FaceLib resource the game ever seeds; every call site used to pass it
|
|
// as an argument and the path predicate below hard-codes the same name.
|
|
static constexpr char kFaceLibResourceName[] = "RFL_Res.dat";
|
|
|
|
bool SeedFaceLibResource(const std::filesystem::path& hostPath) {
|
|
std::vector<uint8_t> payload;
|
|
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
|
|
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(arcPath, ec)) {
|
|
ExtractFromU8(arcPath, kFaceLibResourceName, payload);
|
|
}
|
|
}
|
|
|
|
if (payload.empty()) {
|
|
LogNandError("FaceLibSeed", "Failed to locate %s in RFLRes01.arc", kFaceLibResourceName);
|
|
return false;
|
|
}
|
|
|
|
CreateParentDirectories(hostPath);
|
|
|
|
std::ofstream out(hostPath, std::ios::binary);
|
|
if (!out) {
|
|
LogNandError("FaceLibSeed", "Failed to create %s", HostPathText(hostPath).c_str());
|
|
return false;
|
|
}
|
|
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
|
|
if (!out) {
|
|
LogNandError("FaceLibSeed", "Failed to write %s", HostPathText(hostPath).c_str());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsFaceLibResourcePath(const char* path) {
|
|
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0;
|
|
}
|
|
|
|
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
|
|
const std::filesystem::path& hostPath, int mode, bool ios) {
|
|
const auto action = RuntimeNandSave::CheckRead(hostPath, mode);
|
|
if (action == RuntimeNandSave::ReadAction::Proceed) return std::nullopt;
|
|
if (action == RuntimeNandSave::ReadAction::Missing) {
|
|
LogNandWarning(who, "treating empty or zero-filled system save '%s' as missing",
|
|
HostPathText(hostPath).c_str());
|
|
return ios ? ISFS_ENOENT : NAND_RESULT_NOEXISTS;
|
|
}
|
|
if (action == RuntimeNandSave::ReadAction::RecoveryNeeded) {
|
|
LogNandError(who, "system save '%s' is missing or blank but its .nandsafe.tmp contains data; "
|
|
"back up both files before attempting recovery",
|
|
HostPathText(hostPath).c_str());
|
|
} else {
|
|
LogNandError(who, "could not inspect system save '%s' or its write shadow; leaving data untouched",
|
|
HostPathText(hostPath).c_str());
|
|
}
|
|
return ios ? ISFS_EIO : NAND_RESULT_UNKNOWN;
|
|
}
|
|
|
|
// Create directories recursively
|
|
bool CreateDirectoryPath(const std::filesystem::path& path) {
|
|
if (path.empty()) {
|
|
return true;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(path, ec);
|
|
return !ec || std::filesystem::is_directory(path, ec);
|
|
}
|
|
|
|
// Check if a path exists
|
|
bool PathExists(const std::filesystem::path& path) {
|
|
std::error_code ec;
|
|
return std::filesystem::exists(path, ec) && !ec;
|
|
}
|
|
|
|
// Check if path is a directory
|
|
bool IsDirectory(const std::filesystem::path& path) {
|
|
std::error_code ec;
|
|
return std::filesystem::is_directory(path, ec) && !ec;
|
|
}
|
|
|
|
bool CreateParentDirectories(const std::filesystem::path& path) {
|
|
if (!path.has_parent_path()) {
|
|
return false;
|
|
}
|
|
CreateDirectoryPath(path.parent_path());
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// stdio helpers shared by the NAND* library and the IOS_* device layer
|
|
// ============================================================================
|
|
|
|
int NandSeekOrigin(int32_t whence) {
|
|
if (whence == 1) return SEEK_CUR;
|
|
if (whence == 2) return SEEK_END;
|
|
return SEEK_SET;
|
|
}
|
|
|
|
NandFileExtent NandProbeFileExtent(FILE* file) {
|
|
NandFileExtent extent;
|
|
extent.position = std::ftell(file);
|
|
std::fseek(file, 0, SEEK_END);
|
|
extent.size = std::ftell(file);
|
|
std::fseek(file, extent.position, SEEK_SET);
|
|
return extent;
|
|
}
|