#pragma once #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif namespace RuntimeNandSettings { using Settings = std::map; inline std::filesystem::path FilePath(const std::filesystem::path& root) { return root / "title/00000001/00000002/data/setting.txt"; } // Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key. inline std::optional Read(const std::filesystem::path& nandRoot) { std::ifstream input(FilePath(nandRoot), std::ios::binary); std::array bytes{}; if (!input.read(reinterpret_cast(bytes.data()), bytes.size())) { return std::nullopt; } uint32_t key = 0x73B5DBFAu; std::string decoded; for (const uint8_t byte : bytes) { const char value = static_cast(byte ^ static_cast(key)); key = (key << 1) | (key >> 31); if (value == '\0') { break; } if (value != '\r') { decoded += value; } } Settings settings; for (size_t start = 0; start < decoded.size();) { const size_t end = decoded.find('\n', start); const std::string line = decoded.substr(start, end - start); const size_t equals = line.find('='); if (equals != std::string::npos && equals != 0) { settings.emplace(line.substr(0, equals), line.substr(equals + 1)); } if (end == std::string::npos) { break; } start = end + 1; } return settings; } inline bool HasIdentity(const Settings& settings) { const auto serial = settings.find("SERNO"); if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 || serial->second.find_first_not_of("0123456789") != std::string::npos || serial->second.find_first_not_of('0') == std::string::npos) { return false; } for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) { const auto value = settings.find(field.first); if (value == settings.end() || value->second.empty() || value->second.size() > field.second) { return false; } } return true; } // Dolphin's normal (non-deterministic) first-boot algorithm. It is independent // of the ES device ID. Matching another NAND requires that NAND's saved serial. inline std::string GenerateSerial(std::time_t now) { if (now < 0) { return {}; } const auto digits = std::to_string(now % 1000000000); return std::string(9 - digits.size(), '0') + digits; } // This recompilation targets the European disc. These are Dolphin's PAL boot // defaults; an existing setting.txt always takes precedence, in every region. inline std::optional> EncodeNew(const std::string& serial) { const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}}; if (!HasIdentity(identity)) { return std::nullopt; } std::array bytes{}; size_t position = 0; uint32_t key = 0x73B5DBFAu; const auto writeByte = [&](char value) { bytes[position++] = static_cast(value) ^ static_cast(key); key = (key << 1) | (key >> 31); }; for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"), std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"), "SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) { for (;;) { if (position + line.size() > bytes.size()) { return std::nullopt; } const auto start = position; const auto savedKey = key; bool hasNull = false; for (const char value : line) { writeByte(value); hasNull |= bytes[position - 1] == 0; } if (!hasNull) { break; } // Nintendo stops at an encoded NUL. Dolphin inserts an extra LF // before this line and retries with the shifted encryption key. position = start; key = savedKey; writeByte('\n'); } } return bytes; // The unused tail stays raw zero, as in Dolphin. } // Never replace an existing file, including an unreadable or damaged one. // Publish a complete file atomically so simultaneous launches use one identity. inline bool Ensure(const std::filesystem::path& root, std::string& error, std::time_t now = std::time(nullptr)) { const auto path = FilePath(root); std::error_code ec; const auto status = std::filesystem::symlink_status(path, ec); if (ec && ec != std::errc::no_such_file_or_directory) { error = "Cannot inspect NAND setting.txt: " + ec.message(); return false; } if (std::filesystem::exists(status)) { const auto existing = Read(root); if (existing && HasIdentity(*existing)) { return true; } error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup"; return false; } const auto bytes = EncodeNew(GenerateSerial(now)); if (!bytes) { error = "Cannot initialize NAND settings: invalid system clock"; return false; } ec.clear(); std::filesystem::create_directories(path.parent_path(), ec); if (ec) { error = "Cannot create NAND settings directory: " + ec.message(); return false; } static std::atomic sequence{0}; const auto scratch = path.parent_path() / (".setting-init-" + std::to_string( std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(sequence++)); if (!std::filesystem::create_directory(scratch, ec)) { error = "Cannot create temporary NAND settings directory: " + ec.message(); return false; } const auto temporary = scratch / "setting.txt"; bool written = false; { std::ofstream output(temporary, std::ios::binary); output.write(reinterpret_cast(bytes->data()), bytes->size()); output.close(); written = static_cast(output); } bool published = false; if (written) { #ifdef _WIN32 published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0; #else published = ::link(temporary.c_str(), path.c_str()) == 0; #endif } std::filesystem::remove(temporary, ec); std::filesystem::remove(scratch, ec); // A competing launcher may have published its settings first. Always read // the winner from NAND rather than using our unpersisted candidate serial. const auto persisted = Read(root); if (persisted && HasIdentity(*persisted)) { return true; } error = published ? "Cannot read newly initialized NAND setting.txt" : "Cannot persist NAND setting.txt; check NAND directory permissions"; return false; } } // namespace RuntimeNandSettings