diff --git a/.gitignore b/.gitignore index 855cfed..59542aa 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ Code.pul /build/ /build-*/ /native-build/ +/native-build-macos/ +/local-products/ /dist/ /out/ [Bb]in/ diff --git a/runtime/src/hle/storage/nand_api.cpp b/runtime/src/hle/storage/nand_api.cpp index 723569c..ed1ae24 100644 --- a/runtime/src/hle/storage/nand_api.cpp +++ b/runtime/src/hle/storage/nand_api.cpp @@ -93,6 +93,9 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t const std::filesystem::path hostPath = TranslateNandPath(path); + if (NandIgnoreUninitializedSaveRead("NANDOpen", hostPath, mode)) + return NAND_RESULT_NOEXISTS; + // Existing-file write opens go through a shadow copy seeded from the original, so a // crash between NANDWrite and NANDClose cannot leave a torn file (the game patches // sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place. @@ -136,10 +139,7 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t } } - const char* fopenMode = "rb"; - if (mode == 1) fopenMode = "rb"; - else if (mode == 2) fopenMode = "r+b"; - else if (mode == 3) fopenMode = "r+b"; + const char* fopenMode = (mode == 2 || mode == 3) ? "r+b" : "rb"; FILE* file = NandFopen(hostPath, fopenMode); if (!file && mode >= 2) { diff --git a/runtime/src/hle/storage/nand_async.cpp b/runtime/src/hle/storage/nand_async.cpp index ae9f3d9..51a659f 100644 --- a/runtime/src/hle/storage/nand_async.cpp +++ b/runtime/src/hle/storage/nand_async.cpp @@ -411,6 +411,8 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint if (mode == 1) { // Read-only safe open reads the original in place; the library builds no scratch // copy for this case. + if (NandIgnoreUninitializedSaveRead("NANDSafeOpen", hostPath, mode)) + return NAND_RESULT_NOEXISTS; FILE* file = NandFopen(hostPath, "rb"); if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { file = NandFopen(hostPath, "rb"); diff --git a/runtime/src/hle/storage/nand_fs.cpp b/runtime/src/hle/storage/nand_fs.cpp index 5b35e16..a11a6b7 100644 --- a/runtime/src/hle/storage/nand_fs.cpp +++ b/runtime/src/hle/storage/nand_fs.cpp @@ -411,6 +411,39 @@ bool IsFaceLibResourcePath(const char* path) { return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0; } +bool NandSystemSaveIsUninitialized(const std::filesystem::path& hostPath) { + if (!IsNandSystemSavePath(hostPath)) + return false; + + std::error_code ec; + if (!std::filesystem::is_regular_file(hostPath, ec) || ec) + return false; + + std::ifstream in(hostPath, std::ios::binary); + if (!in) + return false; + + char block[4096]; + while (in) { + in.read(block, sizeof(block)); + const std::streamsize got = in.gcount(); + + for (std::streamsize i = 0; i < got; ++i) + if (block[i] != 0) + return false; + } + return true; +} + +bool NandIgnoreUninitializedSaveRead(const char* who, + const std::filesystem::path& hostPath, int mode) { + if (mode != 1 || !NandSystemSaveIsUninitialized(hostPath)) + return false; + LogNandWarning(who, "ignoring uninitialized system save '%s' (no save committed yet)", + HostPathText(hostPath).c_str()); + return true; +} + // Create directories recursively bool CreateDirectoryPath(const std::filesystem::path& path) { if (path.empty()) { diff --git a/runtime/src/hle/storage/nand_internal.h b/runtime/src/hle/storage/nand_internal.h index f45c617..5309ab1 100644 --- a/runtime/src/hle/storage/nand_internal.h +++ b/runtime/src/hle/storage/nand_internal.h @@ -56,6 +56,25 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback void LogNandError(const char* func, const char* fmt, ...); void LogNandWarning(const char* func, const char* fmt, ...); +// The Mario Kart Wii system save (rksys.dat) and its ".nandsafe.tmp" write shadows. +inline bool IsNandSystemSavePath(const std::filesystem::path& path) { + const std::string name = path.filename().string(); + return name == "rksys.dat" || name.rfind("rksys.dat", 0) == 0; +} + +// True when a system save exists on the host but holds no committed save yet: the game +// zero-fills rksys.dat during its first-run "format save data" step and only writes the +// real database (which always begins with the RKSD0006 header) once it actually saves. +// An all-zero file therefore contains nothing worth loading; read it as absent so the +// game recreates its save instead of entering the corrupt-save recovery loop. +bool NandSystemSaveIsUninitialized(const std::filesystem::path& hostPath); + +// Read-open helper: logs and returns true when a read of this system save should see +// "no save". Centralizes the mode == 1 guard and warning so the NAND, NANDSafe and IOS +// open paths share one branch and message. +bool NandIgnoreUninitializedSaveRead(const char* who, const std::filesystem::path& hostPath, + int mode); + // ============================================================================ // File Descriptor Management // ============================================================================ diff --git a/runtime/src/hle/storage/nand_isfs.cpp b/runtime/src/hle/storage/nand_isfs.cpp index 6ea0ad4..05e70c3 100644 --- a/runtime/src/hle/storage/nand_isfs.cpp +++ b/runtime/src/hle/storage/nand_isfs.cpp @@ -391,6 +391,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) { // It's a NAND file path const std::filesystem::path hostPath = TranslateNandPath(path); + + if (NandIgnoreUninitializedSaveRead("IOS_Open", hostPath, mode)) + return ISFS_ENOENT; // Seed FaceLib resources before the existence check so every open mode can // still find them on a fresh managed NAND.