From 5d67b229f6c94461e7156fdf397638f2aa873442 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:54:24 +0200 Subject: [PATCH 1/5] Update settings_overlay.cpp --- runtime/src/settings_overlay.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 0429be6..8856ba5 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -483,11 +483,6 @@ void DrawRumbleSettings() { PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, }; PADControlAllMotors(stopAll.data()); -#if defined(_WIN32) - for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) { - Wup028Adapter::SetRumble(port, false); - } -#endif } } if (ImGui::IsItemHovered()) { From d1d80613cce30c98a2f5ed872dcd22241f1363a0 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:00:23 +0200 Subject: [PATCH 2/5] Load console identity from NAND setting.txt (#164) --- runtime/CMakeLists.txt | 5 ++ runtime/include/console_identity.h | 109 +++++--------------------- runtime/include/nand_settings.h | 69 ++++++++++++++++ runtime/src/hle/sc.cpp | 44 +++++++---- runtime/tests/nand_settings_tests.cpp | 65 +++++++++++++++ 5 files changed, 188 insertions(+), 104 deletions(-) create mode 100644 runtime/include/nand_settings.h create mode 100644 runtime/tests/nand_settings_tests.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index be057c7..df8418e 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -277,6 +277,11 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform) target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) +add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp") +target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") +target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17) +add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests) + # The input expression engine is self-contained, so it can be exercised without # linking the runtime or SDL. add_executable(mkw_input_expr_tests diff --git a/runtime/include/console_identity.h b/runtime/include/console_identity.h index ef46679..c07fc30 100644 --- a/runtime/include/console_identity.h +++ b/runtime/include/console_identity.h @@ -1,38 +1,28 @@ #pragma once -#include "runtime_config.h" +#include "nand_path.h" +#include "nand_settings.h" -#include #include -#include #include #include -#include -#include #include -#include -#include #include #include -#include #include namespace RuntimeConsoleIdentity { struct Identity { std::string serial; + std::string productCode; + std::string area; + std::string gameRegion; std::array mac; }; -inline bool IsValidSerial(const std::string& serial) { - return serial.size() == 9 && - serial != "000000000" && - std::all_of(serial.begin(), serial.end(), - [](unsigned char value) { return std::isdigit(value) != 0; }); -} - inline Identity FromSerial(std::string serial) { - // Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial + // Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial // so every API exposes one coherent, stable virtual-console identity. uint32_t hash = 2166136261u; for (const unsigned char value : serial) { @@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) { return { std::move(serial), + {}, {}, {}, { 0x00, 0x09, @@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) { }; } -inline std::optional ReadSerial(const std::filesystem::path& path) { - std::ifstream input(path); - std::string line; - if (!input || !std::getline(input, line)) { - return std::nullopt; +inline Identity LoadFromNand() { + const auto root = RuntimeNandPath::DiscoverNandRootPath(); + const auto settings = RuntimeNandSettings::Read(root); + if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) { + RuntimeNandPath::FailNandRoot( + "NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)", + root / "title/00000001/00000002/data/setting.txt"); } - constexpr std::string_view prefix = "serial="; - if (line.rfind(prefix, 0) != 0) { - return std::nullopt; - } - std::string serial = line.substr(prefix.size()); - if (!IsValidSerial(serial)) { - return std::nullopt; - } - return serial; -} - -inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) { - std::error_code ec; - std::filesystem::create_directories(path.parent_path(), ec); - if (ec) { - return false; - } - - std::filesystem::path temporary = path; - temporary += ".tmp"; - { - std::ofstream output(temporary, std::ios::trunc); - if (!output) { - return false; - } - output << "serial=" << serial << '\n'; - output.close(); - if (!output) { - return false; - } - } - - std::filesystem::rename(temporary, path, ec); - if (!ec) { - return true; - } - std::filesystem::remove(temporary, ec); - return false; -} - -inline std::string GenerateSerial() { - std::random_device entropy; - std::seed_seq seed{ - entropy(), - entropy(), - entropy(), - entropy(), - }; - std::mt19937 generator(seed); - std::uniform_int_distribution distribution(100000000u, 999999999u); - return std::to_string(distribution(generator)); -} - -inline Identity LoadOrCreate(const std::filesystem::path& path) { - if (const auto serial = ReadSerial(path)) { - return FromSerial(*serial); - } - - const std::string generated = GenerateSerial(); - if (WriteSerial(path, generated)) { - return FromSerial(generated); - } - - // Remain operational in a read-only environment. This fallback matches - // Dolphin's deterministic serial while keeping the same valid identity shape. - return FromSerial("123456789"); + Identity identity = FromSerial(settings->at("SERNO")); + identity.productCode = settings->at("CODE"); + identity.area = settings->at("AREA"); + identity.gameRegion = settings->at("GAME"); + return identity; } inline const Identity& Current() { - static const Identity identity = - LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt"); + static const Identity identity = LoadFromNand(); return identity; } diff --git a/runtime/include/nand_settings.h b/runtime/include/nand_settings.h new file mode 100644 index 0000000..4d9d947 --- /dev/null +++ b/runtime/include/nand_settings.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RuntimeNandSettings { + +using Settings = std::map; + +// 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(nandRoot / "title/00000001/00000002/data/setting.txt", + 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; +} + +} // namespace RuntimeNandSettings diff --git a/runtime/src/hle/sc.cpp b/runtime/src/hle/sc.cpp index 62c4411..e587455 100644 --- a/runtime/src/hle/sc.cpp +++ b/runtime/src/hle/sc.cpp @@ -12,7 +12,25 @@ namespace { -constexpr uint32_t kPalProductRegion = 2; +// Use the SDK's own value tables, including its unknown-region result. +uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count, + const std::string& value) { + for (uint32_t index = 0; index < count; ++index) { + const uint32_t entry = table + index * stride; + if (!Memory::Contains(entry, stride)) { + break; + } + const auto* bytes = static_cast(Memory::GetPointer(entry, stride)); + if (bytes[0] == 0xFF) { + break; + } + if (value.size() < stride - 1 && + std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) { + return bytes[0]; + } + } + return 0xFFFFFFFFu; +} } // namespace @@ -50,16 +68,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE() PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ()); -// The managed NAND intentionally starts without a console-owned setting.txt. -// DWC nevertheless requires the Wii product code and serial number so it can -// include csnum in NAS authentication. Expose one stable virtual-console -// identity without requiring or mutating a user's real NAND. +// Expose the selected emulated NAND identity through the SDK SC APIs. extern "C" uint32_t SCGetProductArea_HLE() { - // The PAL setting.txt AREA value is "EUR". The SDK's lookup table at - // 0x8029CEB0 maps JPN=0, USA=1, EUR=2. - return kPalProductRegion; + return LookupProductRegion(0x8029CEB0u, 5, 13, + RuntimeConsoleIdentity::Current().area); } PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ()); @@ -68,12 +82,13 @@ extern "C" uint32_t SCGetProductCode_HLE() { // Original PAL SC storage for the six-byte CODE value. constexpr uint32_t kProductCodeAddress = 0x803869E0u; - static constexpr char kProductCode[] = "LEH"; - if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) { + const std::string& productCode = RuntimeConsoleIdentity::Current().productCode; + const size_t size = productCode.size() + 1; + if (!Memory::Contains(kProductCodeAddress, size)) { return 0; } - std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)), - kProductCode, sizeof(kProductCode)); + std::memcpy(Memory::GetPointer(kProductCodeAddress, size), + productCode.c_str(), size); return kProductCodeAddress; } @@ -94,9 +109,8 @@ PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddr extern "C" uint32_t SCGetProductGameRegion_HLE() { - // The PAL setting.txt GAME value is "EU". The SDK's own lookup table at - // 0x8029CEF8 maps JP=0, US=1, EU=2. - return kPalProductRegion; + return LookupProductRegion(0x8029CEF8u, 4, 4, + RuntimeConsoleIdentity::Current().gameRegion); } PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ()); diff --git a/runtime/tests/nand_settings_tests.cpp b/runtime/tests/nand_settings_tests.cpp new file mode 100644 index 0000000..12a8140 --- /dev/null +++ b/runtime/tests/nand_settings_tests.cpp @@ -0,0 +1,65 @@ +#include "nand_settings.h" + +#include +#include +#include + +static void Require(bool condition) { + if (!condition) { + throw std::runtime_error("NAND settings check failed"); + } +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("wiicomp-nand-settings-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + const auto path = root / "title/00000001/00000002/data/setting.txt"; + try { + Require(!RuntimeNandSettings::Read(root)); + Require(!std::filesystem::exists(root)); + std::filesystem::create_directories(path.parent_path()); + const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n"; + std::array fixture{}; + for (size_t i = 0; i < fixture.size(); ++i) { + const unsigned shift = i % 32; + const uint32_t key = shift == 0 ? 0x73B5DBFAu : + (0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift)); + fixture[i] = static_cast(key) ^ (i < plain.size() ? plain[i] : 0); + } + { + std::ofstream output(path, std::ios::binary); + output.write(reinterpret_cast(fixture.data()), fixture.size()); + } + auto settings = RuntimeNandSettings::Read(root); + Require(settings && RuntimeNandSettings::HasIdentity(*settings)); + Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU"); + Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US"); + std::array after{}; + { + std::ifstream input(path, std::ios::binary); + input.read(reinterpret_cast(after.data()), after.size()); + } + Require(after == fixture); + for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) { + (*settings)["SERNO"] = serial; + Require(!RuntimeNandSettings::HasIdentity(*settings)); + } + (*settings)["SERNO"] = "012345678"; + Require(RuntimeNandSettings::HasIdentity(*settings)); + (*settings)["CODE"] = "TOOLONG"; + Require(!RuntimeNandSettings::HasIdentity(*settings)); + (*settings)["CODE"] = "LEH"; + settings->erase("GAME"); + Require(!RuntimeNandSettings::HasIdentity(*settings)); + std::filesystem::resize_file(path, 128); + Require(!RuntimeNandSettings::Read(root)); + std::filesystem::remove_all(root); + std::cout << "NAND settings checks passed\n"; + return 0; + } catch (const std::exception& error) { + std::filesystem::remove_all(root); + std::cerr << error.what() << '\n'; + return 1; + } +} From 1c0a3edee9a97deff13ea92fb023ee3c577cfb8c Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:00:47 +0200 Subject: [PATCH 3/5] Add synthetic recompilation CI workflow (#161) --- .github/workflows/build.yml | 4 + .github/workflows/package.yml | 6 +- .github/workflows/recomp-test.yml | 41 ++++++++ Launcher/Test-Recompilation.ps1 | 146 +++++++++++++++++++++++++++++ Launcher/prepare-portable-tools.sh | 26 ++--- 5 files changed, 209 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/recomp-test.yml create mode 100644 Launcher/Test-Recompilation.ps1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f258e14..267a34e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,10 @@ concurrency: cancel-in-progress: true jobs: + recompilation: + name: Recompilation test + uses: ./.github/workflows/recomp-test.yml + translator: name: Translator (build + test) runs-on: windows-latest diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 742418f..b97c62a 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -20,6 +20,10 @@ concurrency: cancel-in-progress: true jobs: + recompilation: + name: Recompilation test + uses: ./.github/workflows/recomp-test.yml + linux-appimage: name: Linux (AppImage, ${{ matrix.arch }}) strategy: @@ -93,7 +97,7 @@ jobs: release: name: Publish GitHub Release if: startsWith(github.ref, 'refs/tags/v') - needs: [linux-appimage, windows-installer] + needs: [linux-appimage, windows-installer, recompilation] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/recomp-test.yml b/.github/workflows/recomp-test.yml new file mode 100644 index 0000000..0908d15 --- /dev/null +++ b/.github/workflows/recomp-test.yml @@ -0,0 +1,41 @@ +name: Synthetic recompilation + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + windows: + name: Windows runtime (synthetic DOL) + runs-on: windows-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + + # Cache downloads only. Preparation still validates pins, and every run + # compiles current Aurora, runtime, and generated sources from scratch. + - uses: actions/cache@v5 + with: + path: Launcher/artifacts/downloads + key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }} + + - name: Prepare the shipped Windows toolchain + shell: pwsh + run: ./Launcher/Prepare-PortableTools.ps1 + + - name: Prepare pinned native dependencies + shell: pwsh + run: ./Launcher/Prepare-Dependencies.ps1 + + - name: Translate, compile the full runtime, and link + shell: pwsh + run: ./Launcher/Test-Recompilation.ps1 -Parallel 3 diff --git a/Launcher/Test-Recompilation.ps1 b/Launcher/Test-Recompilation.ps1 new file mode 100644 index 0000000..9a8d8a2 --- /dev/null +++ b/Launcher/Test-Recompilation.ps1 @@ -0,0 +1,146 @@ +# Build the real Windows runtime with translated, entirely synthetic PowerPC code. +# No game dump, game symbol map, REL, mod download, or existing generated/ output is used. +[CmdletBinding()] +param( + [string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools', + [string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies', + [string]$StageDirectory = 'build/recomp-test', + [ValidateRange(1, 64)] [int]$Parallel = 3 +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3.0 +. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') + +$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +function Full([string]$Path) { + if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) } + return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path)) +} +$portableTools = Full $PortableToolsDirectory +$dependencies = Full $DependencySourceDirectory +$stage = Full $StageDirectory +$dotnet = (Get-Command dotnet -CommandType Application).Source +$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe' +$ninja = Join-Path $portableTools 'Ninja/ninja.exe' +$compilerBin = Join-Path $portableTools 'llvm-mingw/bin' +Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)' +Assert-File $ninja 'Pinned Ninja' +Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)' + +# Refuse reuse so a developer's game translation or an earlier build cannot make +# the test pass. Keep the staging tree after the run for diagnostics. +if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" } +[IO.Directory]::CreateDirectory($stage) | Out-Null +Write-Host "Synthetic recompilation workspace: $stage" + +# Copy current sources, including uncommitted edits, but no ignored build output. +# An isolated workspace preserves the developer's real generated/ directory. +$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main +if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' } +foreach ($relative in $sourceFiles | Sort-Object -Unique) { + $destination = Join-Path $stage $relative + [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null + Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination +} + +# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr. +# Native HLE wrappers also call these eight guest symbols directly. Give each +# its own generated blr function so the real product can link without game code. +# Keep this list explicit: a new unresolved guest dependency must fail the test. +[uint32]$entry = 0x80001000L +[uint32[]]$guestCallbacks = @( + 0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL, + 0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL +) +$textSize = [int]($guestCallbacks[-1] - $entry + 4) +$dataOffset = 0x100 + $textSize +$dol = [byte[]]::new($dataOffset + 4) +function Write-BigEndian32([int]$Offset, [uint32]$Value) { + $dol[$Offset] = [byte](($Value -shr 24) -band 255) + $dol[$Offset + 1] = [byte](($Value -shr 16) -band 255) + $dol[$Offset + 2] = [byte](($Value -shr 8) -band 255) + $dol[$Offset + 3] = [byte]($Value -band 255) +} +Write-BigEndian32 0x00 0x100 # text[0] file offset +Write-BigEndian32 0x48 $entry # text[0] guest address +Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero) +Write-BigEndian32 0x1C $dataOffset # data[0] file offset +Write-BigEndian32 0x64 0x80600000L # data[0] guest address +Write-BigEndian32 0xAC 4 # data[0] length +Write-BigEndian32 0xD8 0x80601000L # BSS address +Write-BigEndian32 0xDC 32 # BSS length +Write-BigEndian32 0xE0 $entry # entry point +Write-BigEndian32 0x100 0x38600028 # li r3,40 +Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2 +Write-BigEndian32 0x108 0x60000000 # nop +Write-BigEndian32 0x10C 0x4E800020 # blr +foreach ($address in $guestCallbacks) { + Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020 +} +Write-BigEndian32 $dataOffset 0x12345678 +[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol) +$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', ' +$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n" +[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap) +$manifest = Join-Path $stage 'recomp.yml' +[IO.File]::WriteAllText($manifest, @" +schema_version: 1 +workspace_root: . +project: + id: ci-synthetic-dol + display_name: CI Synthetic DOL +memory: + base: 0x80000000 + size: 0x01800000 + sda_base: 0x80600000 + sda2_base: 0x80600000 +inputs: + dol: + path: synthetic.dol +translation: + entry_points: [$entryPoints] + function_map: + path: synthetic-functions.txt + allow_unsupported_instructions: false +runtime: + native_abi_directories: [] + native_registration_root: runtime/src +output: + root: generated +"@) + +$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj' +Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator' +$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll' +$metadata = Join-Path $stage 'generated/base_translation_output.json' +Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest, + '--output-metadata', $metadata, '--threads', "$Parallel") ` + 'Translating the synthetic DOL' +# Function-map seeds can be skipped by discovery; do not accept a partial fixture. +$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json +foreach ($address in @($entry) + $guestCallbacks) { + if ($address -notin $translated.functions.entryPoint) { + throw ('Synthetic function 0x{0:X8} was not translated.' -f $address) + } +} +Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration' +Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph' + +$nativeBuild = Join-Path $stage 'native-build' +$oldPath = $env:PATH +try { + $env:PATH = Get-MkwToolchainPath $portableTools + $configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild ` + -Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') ` + -CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') ` + -ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') ` + -DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON') + Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' + Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") ` + 'Compiling and linking the synthetic product with the full runtime' + Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product' +} finally { + $env:PATH = $oldPath +} +Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).' diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh index 8e812ff..69ea00d 100755 --- a/Launcher/prepare-portable-tools.sh +++ b/Launcher/prepare-portable-tools.sh @@ -201,10 +201,10 @@ Ninja $ninja_version Apache License 2.0 EOF -echo "prepare-portable-tools.sh: smoke-testing the toolchain..." -smoke_dir=$(mktemp -d) -trap 'rm -rf "$smoke_dir"' EXIT -cat > "$smoke_dir/t.cpp" <<'EOF' +echo "prepare-portable-tools.sh: testing the toolchain..." +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT +cat > "$test_dir/t.cpp" <<'EOF' #include #include int main() { @@ -214,23 +214,23 @@ int main() { return sum == 6 ? 0 : 1; } EOF -"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t" -"$smoke_dir/t" +"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t" +"$test_dir/t" # Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain # clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that # can't find the compiler. -cat > "$smoke_dir/CMakeLists.txt" <<'EOF' +cat > "$test_dir/CMakeLists.txt" <<'EOF' cmake_minimum_required(VERSION 3.16) -project(smoke CXX) -add_executable(smoke t.cpp) +project(test CXX) +add_executable(test t.cpp) EOF -"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \ +"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \ -DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null -"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null -"$smoke_dir/build/smoke" +"$work/bin/cmake" --build "$test_dir/build" >/dev/null +"$test_dir/build/test" -rm -rf "$smoke_dir" +rm -rf "$test_dir" trap - EXIT mv "$work" "$toolchain_dir" From 8e57cc162f48d3f9398cc332929f8ebc9c3ea699 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:48:41 +0200 Subject: [PATCH 4/5] version --- Launcher/Build-Installer.ps1 | 2 +- .../WiiCompiled.Setup.Common.Cli.csproj | 2 +- .../WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj | 2 +- Launcher/WiiCompiled.Setup.Linux/Models.cs | 2 +- Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj | 2 +- Launcher/WiiCompiled.Setup.Windows/Program.cs | 2 +- .../WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Launcher/Build-Installer.ps1 b/Launcher/Build-Installer.ps1 index ec95fbf..4bb7b63 100644 --- a/Launcher/Build-Installer.ps1 +++ b/Launcher/Build-Installer.ps1 @@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo $manifest = [ordered]@{ SchemaVersion = 2 - ProductVersion = '0.2.28' + ProductVersion = '0.2.29' ExpectedGameId = $pins.GameId ExpectedDolSha256 = $pins.DolSha256 ExpectedRelSha256 = $pins.RelSha256 diff --git a/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj b/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj index 4f7cad6..2519f54 100644 --- a/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj +++ b/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj @@ -6,7 +6,7 @@ enable WiiCompiled.Setup.Common.Cli WiiCompiled.Setup.Common.Cli - 0.2.28 + 0.2.29 patchzy WiiCompiled Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1 diff --git a/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj b/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj index 7cbc650..417e48a 100644 --- a/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj +++ b/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj @@ -5,7 +5,7 @@ enable WiiCompiled.Setup.Common WiiCompiled.Setup.Common - 0.2.28 + 0.2.29 patchzy WiiCompiled Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers diff --git a/Launcher/WiiCompiled.Setup.Linux/Models.cs b/Launcher/WiiCompiled.Setup.Linux/Models.cs index 59b4984..8102c7b 100644 --- a/Launcher/WiiCompiled.Setup.Linux/Models.cs +++ b/Launcher/WiiCompiled.Setup.Linux/Models.cs @@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux; internal static class ProductInfo { public const string Name = "WiiCompiled"; - public const string Version = "0.2.28"; + public const string Version = "0.2.29"; } /// One installed product's record inside install-state.json. diff --git a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj index 6c68fc0..57c6a0f 100644 --- a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj +++ b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj @@ -6,7 +6,7 @@ enable WiiCompiled.Setup.Linux WiiCompiled.Setup.Linux - 0.2.28 + 0.2.29 patchzy WiiCompiled Command-line installer and launcher for WiiCompiled on Linux diff --git a/Launcher/WiiCompiled.Setup.Windows/Program.cs b/Launcher/WiiCompiled.Setup.Windows/Program.cs index 42e5005..09ea084 100644 --- a/Launcher/WiiCompiled.Setup.Windows/Program.cs +++ b/Launcher/WiiCompiled.Setup.Windows/Program.cs @@ -121,7 +121,7 @@ internal static class PlatformChecks internal static class ProductInfo { public const string Name = "WiiCompiled"; - public const string Version = "0.2.28"; + public const string Version = "0.2.29"; /// /// The setup executable is copied into the installation under this name. It is the launcher and diff --git a/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj b/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj index af088e3..d79ecbb 100644 --- a/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj +++ b/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj @@ -7,7 +7,7 @@ WiiCompiled.Setup WiiCompiled.Setup.Windows app.manifest - 0.2.28 + 0.2.29 patchzy WiiCompiled Command-line installer and launcher for WiiCompiled From 28cbea46ab44105fde928653507add5d9d4873f4 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:04:07 +0200 Subject: [PATCH 5/5] import setting.txt --- runtime/CMakeLists.txt | 2 + runtime/include/nand_path.h | 15 ++- runtime/include/nand_settings.h | 134 +++++++++++++++++++++++++- runtime/tests/nand_settings_tests.cpp | 90 ++++++++++++++++- 4 files changed, 236 insertions(+), 5 deletions(-) diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index df8418e..34ff3c3 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -278,6 +278,8 @@ target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp") +find_package(Threads REQUIRED) +target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads) target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17) add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests) diff --git a/runtime/include/nand_path.h b/runtime/include/nand_path.h index 709e4af..eb0fda8 100644 --- a/runtime/include/nand_path.h +++ b/runtime/include/nand_path.h @@ -1,6 +1,7 @@ #pragma once #include "runtime_config.h" +#include "nand_settings.h" #include "runtime_log.h" #include "system_bridge.h" @@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() { return root; } -inline std::filesystem::path DiscoverNandRootPath() { +inline std::filesystem::path ResolveNandRootPath() { const std::string configPath = RuntimeConfigFile::NandRoot(); if (!configPath.empty()) { const auto path = ResolveConfiguredPath(configPath); @@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() { return CreateManagedNandRoot(); } +inline std::filesystem::path DiscoverNandRootPath() { + static const auto root = [] { + const auto resolved = ResolveNandRootPath(); + std::string error; + if (!RuntimeNandSettings::Ensure(resolved, error)) { + FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved)); + } + return resolved; + }(); + return root; +} + } // namespace RuntimeNandPath diff --git a/runtime/include/nand_settings.h b/runtime/include/nand_settings.h index 4d9d947..0fb3e6a 100644 --- a/runtime/include/nand_settings.h +++ b/runtime/include/nand_settings.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -9,14 +12,23 @@ #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(nandRoot / "title/00000001/00000002/data/setting.txt", - std::ios::binary); + std::ifstream input(FilePath(nandRoot), std::ios::binary); std::array bytes{}; if (!input.read(reinterpret_cast(bytes.data()), bytes.size())) { return std::nullopt; @@ -66,4 +78,122 @@ inline bool HasIdentity(const Settings& settings) { 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 diff --git a/runtime/tests/nand_settings_tests.cpp b/runtime/tests/nand_settings_tests.cpp index 12a8140..905071d 100644 --- a/runtime/tests/nand_settings_tests.cpp +++ b/runtime/tests/nand_settings_tests.cpp @@ -3,19 +3,43 @@ #include #include #include +#include +#include -static void Require(bool condition) { +static void Require(bool condition, const char* message = "NAND settings check failed") { if (!condition) { - throw std::runtime_error("NAND settings check failed"); + throw std::runtime_error(message); } } +static std::string ReadBytes(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("wiicomp-nand-settings-" + std::to_string( std::chrono::steady_clock::now().time_since_epoch().count())); const auto path = root / "title/00000001/00000002/data/setting.txt"; try { + using namespace RuntimeNandSettings; + Require(GenerateSerial(1800000123) == "800000123", "Dolphin timestamp modulo"); + Require(GenerateSerial(1000000001) == "000000001", "Dolphin leading zero padding"); + Require(GenerateSerial(-1).empty(), "Invalid clock must not supply an identity"); + // Golden bytes generated by Dolphin's unmodified SettingsHandler.cpp + // (upstream 2026-09-06), PAL boot fields and synthetic serial 000000001. + // Everything after this prefix is raw zero padding to 256 bytes. + const std::string goldenHex = + "bba6ac929a0bc96b7eed83d27f33a1e7e73d9b836d8b47c59ee23df6b275baab" + "bec9d9dead03cc7a3bdafee50c30ab9fb86194e119fe4ba19eff62d5ec3aacb3" + "b5c9d9e3977eac0943d7ff903120a49ef024eafe1cf77be79cf6229a823aabf0f0"; + std::array golden{}; + for (size_t i = 0; i < goldenHex.size() / 2; ++i) { + golden[i] = static_cast(std::stoul(goldenHex.substr(i * 2, 2), nullptr, 16)); + } + Require(EncodeNew("000000001") == golden, "Exact Dolphin writer golden fixture"); + std::string error; Require(!RuntimeNandSettings::Read(root)); Require(!std::filesystem::exists(root)); std::filesystem::create_directories(path.parent_path()); @@ -35,6 +59,7 @@ int main() { Require(settings && RuntimeNandSettings::HasIdentity(*settings)); Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU"); Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US"); + Require(Ensure(root, error, 1800000123), "Existing imported NAND must work"); std::array after{}; { std::ifstream input(path, std::ios::binary); @@ -54,6 +79,67 @@ int main() { Require(!RuntimeNandSettings::HasIdentity(*settings)); std::filesystem::resize_file(path, 128); Require(!RuntimeNandSettings::Read(root)); + const auto damaged = ReadBytes(path); + Require(!Ensure(root, error, 1800000123), "Do not replace a truncated identity"); + Require(ReadBytes(path) == damaged, "Damaged file must remain untouched"); + + const auto fresh = root / "fresh"; + Require(Ensure(fresh, error, 1800000123), "Missing setting.txt must initialize"); + const auto generated = Read(fresh); + Require(generated && HasIdentity(*generated), "Generated file must be readable"); + Require(generated->at("SERNO") == "800000123", "Persist Dolphin-generated serial"); + Require(generated->at("CODE") == "LEH" && generated->at("AREA") == "EUR" && + generated->at("GAME") == "EU", "PAL first-boot fields"); + Require(generated->at("MODEL") == "RVL-001(EUR)" && generated->at("VIDEO") == "PAL" && + generated->at("DVD") == "0" && generated->at("MPCH") == "0x7FFE", + "Complete Dolphin boot settings"); + const auto firstBoot = ReadBytes(FilePath(fresh)); + Require(firstBoot.size() == 256 && firstBoot.back() == 0, "Dolphin buffer size and raw zero padding"); + Require(Ensure(fresh, error, 1900000999), "Second boot"); + Require(ReadBytes(FilePath(fresh)) == firstBoot, "Second boot must not change any bytes"); + + const auto blocked = root / "blocked"; + { std::ofstream output(blocked); output << "file obstructing NAND directory"; } + Require(!Ensure(blocked, error, 1800000123), "Write failure must not return an ephemeral identity"); + Require(!Ensure(root / "bad-clock", error, -1), "Clock failure must not initialize"); + + const auto concurrent = root / "concurrent"; + std::array results{}; + std::vector workers; + for (size_t i = 0; i < results.size(); ++i) { + workers.emplace_back([&, i] { + std::string detail; + results[i] = Ensure(concurrent, detail, 1800000001 + i); + }); + } + for (auto& worker : workers) worker.join(); + for (const bool result : results) Require(result, "Concurrent boot must read the persisted winner"); + const auto winner = ReadBytes(FilePath(concurrent)); + Require(Read(concurrent) && HasIdentity(*Read(concurrent)), "Concurrent boot must persist valid settings"); + Require(Ensure(concurrent, error, 1900000999), "Boot after concurrent initialization"); + Require(ReadBytes(FilePath(concurrent)) == winner, "Concurrent winner must remain stable"); + + // Independently decode as Nintendo does: stop at the first encoded NUL. + // Exercise serials that force Dolphin's extra-LF escaping, not only + // values that happen to work with a plain rotating-XOR encoder. + bool sawExtraLf = false; + for (int serial = 1; serial <= 10000; ++serial) { + const auto number = GenerateSerial(1000000000 + serial); + const auto encoded = EncodeNew(number); + Require(encoded.has_value(), "Serial encoding must fit"); + std::string decoded; + for (size_t i = 0; i < encoded->size() && (*encoded)[i] != 0; ++i) { + const unsigned shift = i % 32; + const uint32_t key = shift == 0 ? 0x73B5DBFAu : + (0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift)); + decoded += static_cast((*encoded)[i] ^ static_cast(key)); + } + Require(decoded.find("SERNO=" + number + "\r\n") != std::string::npos && + decoded.find("GAME=EU\r\n") != std::string::npos, + "Encoded NUL must not truncate settings"); + sawExtraLf |= decoded.find("\r\n\n") != std::string::npos; + } + Require(sawExtraLf, "Exercise Dolphin LF escape path"); std::filesystem::remove_all(root); std::cout << "NAND settings checks passed\n"; return 0;