Files
wiicompiled/Launcher/WiiCompiled.Setup.Windows/InstallerEngine.cs
T
2026-09-05 09:55:09 +02:00

534 lines
29 KiB
C#

using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal sealed class InstallerEngine
{
private readonly IInstallReporter _reporter;
public InstallerEngine(IInstallReporter reporter) => _reporter = reporter;
public async Task InstallAsync(InstallOptions options, CancellationToken cancellationToken = default)
{
var installDirectory = Path.GetFullPath(options.InstallDirectory);
ValidateInstallDirectory(installDirectory);
InputValidation.ValidateExtension(options.GamePath);
var parent = Directory.GetParent(installDirectory)?.FullName
?? throw new InvalidOperationException("The installation directory has no parent.");
Directory.CreateDirectory(parent);
using var operationLock = InstallOperationLock.Acquire(installDirectory, _reporter);
var existing = new Installation(installDirectory);
PortableInstallHealing.HealMovedInstall(existing, _reporter);
// Capture this before publishing anything. An existing installation may be repaired or
// updated by this invocation, but those operations must not recreate the user's shortcuts.
var firstInstall = !existing.IsPresent;
var previousState = existing.ReadInstallState();
using var scratch = Directory.Exists(installDirectory)
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
: InstallScratchSpace.CreateSibling(installDirectory, _reporter);
var staging = scratch.Root;
const long payloadStagingAllowance = 2L * 1024 * 1024 * 1024;
FileSystemUtilities.EnsureFreeSpace(installDirectory, payloadStagingAllowance, "Setup");
using var payload = PayloadArchive.OpenCurrent();
var manifest = payload.ReadManifest();
var toolkit = InstalledLayout.Toolkit(staging);
var workspace = InstalledLayout.Workspace(staging);
var candidateToolkitFingerprint = manifest.ToolkitFingerprint;
var candidateToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint;
var candidateRuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint;
var installedToolkitFingerprint = existing.IsPresent
? existing.ResolveToolkitFingerprint()
: "";
var installedToolkitPackageFingerprint = existing.IsPresent
? existing.ResolveToolkitPackageFingerprint()
: "";
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
StringComparison.Ordinal);
var samePackageContent = existing.IsPresent &&
installedToolkitPackageFingerprint.Equals(
candidateToolkitPackageFingerprint, StringComparison.Ordinal);
var sameToolkitPackage = sameToolkit && samePackageContent;
var hadRetro = existing.HasRetroProduct || previousState?.RetroRewindInstalled == true;
var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing,
candidateRuntimeAssetsFingerprint, cancellationToken);
var installedNodTool = Path.Combine(existing.ToolkitDirectory, "nodtool.exe");
var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent,
File.Exists(installedNodTool));
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
_reporter.Progress(InstallStages.ExtractToolkit,
"Inspecting the release's local recompilation toolkit...", 1);
if (extractToolkit) payload.ExtractDirectory(InstalledLayout.ToolkitDirectoryName, toolkit);
if (extractWorkspace)
{
payload.ExtractDirectory(InstalledLayout.WorkspaceDirectoryName, workspace);
WorkspaceTimestamps.MarkChangedFiles(existing.WorkspaceDirectory, workspace,
_reporter.Diagnostic, cancellationToken);
}
payload.ExtractEntry("host/WiiCompiled-Setup.exe",
Path.Combine(staging, ProductInfo.SetupCopyName));
payload.ExtractDirectory("licenses", Path.Combine(staging, "licenses"));
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
? null
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
ValidateRetroOptions(canonicalRetroRoot, options.RetroWfcPayloadMode, manifest);
var retroCompileInputs = canonicalRetroRoot is null
? null
: SnapshotRetroRewindCompileInputs(canonicalRetroRoot,
Path.Combine(staging, "compile-inputs"), cancellationToken);
if (options.Portable)
{
var portableRoot = PortableRoot.Create(parent);
_reporter.Diagnostic($"Installing as a portable installation under {portableRoot}.");
}
if (sameToolkit)
{
var desiredInputDrift = ProductRepairService.FindDesiredInputDrift(existing,
candidateToolkitFingerprint, manifest.ExpectedDolSha256, manifest.ExpectedRelSha256);
if (desiredInputDrift.Retro && retroCompileInputs is null)
throw new InvalidOperationException(
"This release changes the clean game inputs used by the installed Retro Rewind product. " +
"Supply Wheel Wizard's current Retro Rewind folder explicitly so it can be rebuilt safely.");
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, Path.Combine(staging, "repair"),
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
var remainingCancellation = reconciliation.PublicationCommitted
? CancellationToken.None
: cancellationToken;
if (!runtimeAssetsCurrent)
runtimeAssetsCurrent = RuntimeAssetsAreCurrent(existing,
candidateRuntimeAssetsFingerprint, remainingCancellation);
remainingCancellation.ThrowIfCancellationRequested();
EnsureReconciliationCurrent(existing, candidateToolkitFingerprint, retroCompileInputs,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, updatedState);
remainingCancellation.ThrowIfCancellationRequested();
var releaseEntries = new List<InstallTransactionEntry>();
if (!sameToolkitPackage)
AddComponent(releaseEntries, staging, installDirectory,
InstalledLayout.ToolkitDirectoryName);
if (!runtimeAssetsCurrent)
AddRuntimeAssetPublicationEntries(releaseEntries, staging, installDirectory,
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
remainingCancellation);
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
releaseEntries, remainingCancellation, createShortcuts: firstInstall);
return;
}
if (hadRetro && retroCompileInputs is null)
{
throw new InvalidOperationException(
"The installed Retro Rewind product must be produced again. Supply Wheel Wizard's " +
"current Retro Rewind folder explicitly so it can be rebuilt safely.");
}
EnsureSufficientRemainingDiskSpace(installDirectory,
needsExtractedDisc: reusableGameAssets is null, needsLocalBuild: true);
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(nodTool, options.GamePath,
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
}
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
previousState, options, canonicalRetroRoot, retroCompileInputs,
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
cancellationToken: cancellationToken);
}
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
bool nodToolPresent) =>
!sameToolkit || !samePackageContent || !nodToolPresent;
private static void AddComponent(List<InstallTransactionEntry> entries, string staging,
string installDirectory, string name) =>
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, name),
Path.Combine(installDirectory, name)));
/// <summary>
/// Build-produced workspace directories (not shipped in a release) grafted from the installed workspace into
/// the staged one, carrying the base translation, native build dir, verified inputs, and Retro-WFC payload cache
/// across a toolkit update. Each is re-validated by the build, so stale caches degrade to a clean rebuild, never reuse.
/// </summary>
private static readonly string[] WorkspaceCacheDirectories =
["generated", "build", "native-build", "Assets", "PulsarPacks"];
private async Task PublishToolkitAndReconcileProductsAsync(Installation existing, string staging,
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
{
var installDirectory = existing.Root;
foreach (var cacheDirectory in WorkspaceCacheDirectories)
{
cancellationToken.ThrowIfCancellationRequested();
var source = Path.Combine(existing.WorkspaceDirectory, cacheDirectory);
if (!Directory.Exists(source)) continue;
var destination = Path.Combine(stagedWorkspace, cacheDirectory);
if (Directory.Exists(destination)) Directory.Delete(destination, recursive: true);
Directory.Move(source, destination);
}
var state = BuildInstallState(previousState, installDirectory, manifest,
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, state);
cancellationToken.ThrowIfCancellationRequested();
var entries = new List<InstallTransactionEntry>();
AddComponent(entries, staging, installDirectory, InstalledLayout.ToolkitDirectoryName);
AddComponent(entries, staging, installDirectory, InstalledLayout.WorkspaceDirectoryName);
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
Publish(staging, installDirectory, canonicalRetroRoot, state,
entries, cancellationToken, progressPercent: 8, completionPercent: 10,
createShortcuts: createShortcuts);
_reporter.Progress(InstallStages.BuildBase,
"Producing the installed products with the published toolkit...", 11);
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, repairScratch: null,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
EnsureReconciliationCurrent(existing, manifest.ToolkitFingerprint, retroCompileInputs,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
}
private Task<ProductRepairService.ReconciliationResult> ReconcileSameToolkitAsync(Installation existing,
RetroRewindCompileInputs? retroCompileInputs, string? canonicalRetroRoot,
RetroWfcPayloadMode payloadMode, string? repairScratch, string expectedDolSha256,
string expectedRelSha256, CancellationToken cancellationToken)
{
var repair = new ProductRepairService(existing, _reporter);
var options = new ProductRepairService.ReconcileOptions
{
ScratchRoot = repairScratch,
ExpectedDolSha256 = expectedDolSha256,
ExpectedRelSha256 = expectedRelSha256,
CanonicalRetroRewindRoot = canonicalRetroRoot
};
return retroCompileInputs is null
? repair.RepairBaseAsync(options, cancellationToken)
: repair.RepairRetroAsync(retroCompileInputs, payloadMode, options, cancellationToken);
}
private static void EnsureReconciliationCurrent(Installation installation, string toolkitFingerprint,
RetroRewindCompileInputs? retroCompileInputs, string expectedDolSha256, string expectedRelSha256,
bool? cachedRetroWfcPayloadMatches)
{
var baseState = installation.CheckBase(toolkitFingerprint);
var retroState = installation.CheckRetroRewind(toolkitFingerprint, retroCompileInputs,
canonicalError: null, cachedRetroWfcPayloadMatches);
var desiredInputsCurrent = installation.ProductUsesGameInputs(installation.BaseDirectory,
toolkitFingerprint, expectedDolSha256, expectedRelSha256) &&
(!installation.HasRetroProduct ||
installation.ProductUsesGameInputs(installation.RetroDirectory,
toolkitFingerprint, expectedDolSha256, expectedRelSha256));
if (baseState.Status == ProductStatus.Current && !retroState.ActionRequired && desiredInputsCurrent) return;
var details = string.Join(" ", new[] { baseState, retroState }
.Where(state => state.ActionRequired)
.Select(state => state.Detail)
.Where(detail => !string.IsNullOrWhiteSpace(detail)));
throw new InvalidOperationException(
"Product reconciliation did not produce a current installation with the release's game inputs; " +
"release metadata was not advanced." +
(string.IsNullOrWhiteSpace(details) ? "" : " " + details));
}
/// <summary>
/// Publishes the caller's component entries together with the release metadata every install and
/// update advances, then applies the runtime configuration this installation owns.
/// </summary>
private void Publish(string staging, string installDirectory,
string? canonicalRetroRoot, InstallState state,
List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
int progressPercent = 95, int completionPercent = 99, bool createShortcuts = false)
{
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
Path.Combine(installDirectory, "licenses")));
entries.Add(InstallTransactionEntry.File(Path.Combine(staging, ProductInfo.SetupCopyName),
Path.Combine(installDirectory, ProductInfo.SetupCopyName)));
entries.Add(InstallTransactionEntry.File(
Path.Combine(staging, InstalledLayout.PayloadManifestFileName),
Path.Combine(installDirectory, InstalledLayout.PayloadManifestFileName)));
entries.Add(InstallTransactionEntry.File(Path.Combine(staging, ToolkitState.FileName),
Path.Combine(installDirectory, ToolkitState.FileName)));
entries.Add(InstallTransactionEntry.File(
Path.Combine(staging, InstalledLayout.InstallStateFileName),
Path.Combine(installDirectory, InstalledLayout.InstallStateFileName)));
cancellationToken.ThrowIfCancellationRequested();
RunningProductGuard.EnsureProductsNotRunning(installDirectory);
_reporter.Progress(InstallStages.Publish, "Publishing the completed installation...", progressPercent);
var configPath = RuntimeConfiguration.ResolveConfigPath(installDirectory);
var configSnapshot = RuntimeConfiguration.Capture(configPath);
using var transaction = InstallTransaction.Begin(installDirectory, _reporter, entries.ToArray());
transaction.Publish();
transaction.RecordRuntimeConfigurationMutation(configSnapshot);
RuntimeConfiguration.SetDvdRoot(configPath, Path.Combine(installDirectory, "GameAssets", "DATA"));
// The runtime scans this directory live for its asset overlay, so it is recorded on every
// operation that receives one and is never rewritten by a base-only operation.
if (canonicalRetroRoot is not null)
RuntimeConfiguration.SetRetroRewindRoot(configPath, canonicalRetroRoot);
transaction.Commit();
try
{
// A portable installation is owned by the folder it lives in, not by this machine. Adding
// a machine-wide uninstall entry for it would outlive the folder and, worse, would
// overwrite the entry of a normal installation on the same account.
if (PortableRoot.TryFind(installDirectory) is not null)
{
_reporter.Diagnostic(
"Portable installation: no Windows uninstall entry was registered. Remove the folder, " +
"or run the copied setup with --uninstall, to uninstall it.");
}
else
{
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
}
if (createShortcuts)
ShellIntegration.CreateShortcuts(installDirectory);
}
catch (Exception ex)
{
_reporter.Diagnostic("The installation succeeded, but Windows shell integration failed: " +
ex.Message);
}
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
}
private static void PrepareMetadata(string staging, PayloadManifest manifest, InstallState state)
{
JsonState.Write(Path.Combine(staging, ToolkitState.FileName), new ToolkitState
{
ToolkitFingerprint = manifest.ToolkitFingerprint,
ToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint,
RuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint,
TranslationFingerprint = manifest.TranslationFingerprint,
NativeToolchainFingerprint = manifest.NativeToolchainFingerprint,
ToolkitReleaseTag = manifest.ToolkitReleaseTag
});
JsonState.Write(Path.Combine(staging, InstalledLayout.InstallStateFileName), state);
}
private static InstallState BuildInstallState(InstallState? previousState, string installDirectory,
PayloadManifest manifest, RetroRewindCompileInputs? compileInputs, string? canonicalRetroRoot,
RetroWfcPayloadMode payloadMode,
RetroWfcPayloadSnapshot? retroWfcPayloadSnapshot = null)
{
// Preserve the pre-update object for runtime-configuration rollback/ownership decisions.
var state = new InstallState
{
SchemaVersion = 1,
SetupVersion = ProductInfo.Version,
ProductVersion = manifest.ProductVersion,
InstallDir = installDirectory,
InstalledUtc = previousState?.InstalledUtc ?? DateTime.UtcNow,
RetroRewindInstalled = previousState?.RetroRewindInstalled ?? false,
ToolkitReleaseTag = manifest.ToolkitReleaseTag,
DolSha256 = manifest.ExpectedDolSha256,
RelSha256 = manifest.ExpectedRelSha256,
RetroRewindCodePulSha256 = previousState?.RetroRewindCodePulSha256 ?? "",
RetroRewindCompileInputsSha256 = previousState?.RetroRewindCompileInputsSha256 ?? "",
RetroWfcPayloadMode = previousState?.RetroWfcPayloadMode ?? "",
RetroWfcPayloadSha256 = previousState?.RetroWfcPayloadSha256 ?? "",
RetroWfcPayloadLength = previousState?.RetroWfcPayloadLength ?? 0,
RetroRewindRoot = canonicalRetroRoot is null
? previousState?.RetroRewindRoot ?? ""
: Path.GetFullPath(canonicalRetroRoot)
};
if (compileInputs is not null)
{
state.RetroRewindInstalled = true;
state.RetroRewindCodePulSha256 = compileInputs.CodePulSha256;
state.RetroRewindCompileInputsSha256 = compileInputs.CompileInputsSha256;
state.RetroWfcPayloadMode = payloadMode == RetroWfcPayloadMode.Online
? "downloaded"
: "skipped";
state.RetroWfcPayloadSha256 = payloadMode == RetroWfcPayloadMode.Online
? retroWfcPayloadSnapshot?.Sha256 ?? state.RetroWfcPayloadSha256
: "";
state.RetroWfcPayloadLength = payloadMode == RetroWfcPayloadMode.Online
? retroWfcPayloadSnapshot?.ByteLength ?? state.RetroWfcPayloadLength
: 0;
if (payloadMode == RetroWfcPayloadMode.Online &&
(string.IsNullOrWhiteSpace(state.RetroWfcPayloadSha256) || state.RetroWfcPayloadLength <= 0))
throw new InvalidDataException("The downloaded Retro-WFC payload snapshot is missing.");
}
return state;
}
/// <summary>
/// Captures the canonical installation's compile inputs into operation-owned staging. Only
/// Code.pul and the bundled Pulsar sources are copied; the asset tree stays where Wheel Wizard
/// owns it and is read live by the runtime through <c>retro_rewind_root</c>.
/// </summary>
private RetroRewindCompileInputs SnapshotRetroRewindCompileInputs(string canonicalRetroRoot,
string destination, CancellationToken cancellationToken)
{
_reporter.Progress(InstallStages.Validate, "Snapshotting the Retro Rewind compile inputs...", 3);
return CompileInputsFingerprint.Snapshot(canonicalRetroRoot, destination, cancellationToken);
}
private bool RuntimeAssetsAreCurrent(Installation installation, string expectedFingerprint,
CancellationToken cancellationToken)
{
var installedSource = ToolkitFingerprint.TryComputeRuntimeAssets(installation.Root, cancellationToken,
_reporter.Diagnostic);
if (!expectedFingerprint.Equals(installedSource, StringComparison.Ordinal)) return false;
if (File.Exists(installation.BaseExecutable) &&
!ToolkitFingerprint.ProductRuntimeAssetsMatch(installation.BaseDirectory, expectedFingerprint,
cancellationToken, _reporter.Diagnostic))
return false;
return !installation.HasRetroProduct ||
ToolkitFingerprint.ProductRuntimeAssetsMatch(installation.RetroDirectory,
expectedFingerprint, cancellationToken, _reporter.Diagnostic);
}
private void AddRuntimeAssetPublicationEntries(List<InstallTransactionEntry> entries,
string staging, string installDirectory, bool includeRetro, string expectedFingerprint,
CancellationToken cancellationToken)
{
// The identity was computed from this exact operation-owned payload extraction, so the
// staged assets are published as they are; only repair has an installed source to re-verify.
var sourceAssets = Path.Combine(InstalledLayout.Workspace(staging), "runtime", "assets");
var products = (includeRetro ? new[] { "Base", "RetroRewind" } : new[] { "Base" })
.Select(product => (product, Path.Combine(installDirectory, product)));
RuntimeAssetPublication.AddEntries(entries, sourceAssets,
Path.Combine(staging, "runtime-asset-publication"), products, expectedFingerprint,
"publication", _reporter.Diagnostic, cancellationToken);
entries.Add(InstallTransactionEntry.Directory(sourceAssets,
Path.Combine(InstalledLayout.Workspace(installDirectory), "runtime", "assets")));
}
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode,
PayloadManifest manifest)
{
if (canonicalRetroRoot is null && mode != RetroWfcPayloadMode.NotApplicable)
throw new InvalidOperationException(
"A Retro-WFC payload option requires the canonical Retro Rewind folder.");
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.NotApplicable)
throw new InvalidOperationException(
"Download the Retro-WFC payload, or explicitly skip the optional payload.");
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.Online)
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
}
private string? FindReusableGameAssets(Installation existing, PayloadManifest manifest)
{
var dataRoot = existing.GameDataDirectory;
// No fst.bin simply means nothing was extracted here before; anything past that gate is an
// existing extraction that fails reuse, which the log must say before the expensive
// re-extraction quietly repairs it.
if (!File.Exists(Path.Combine(dataRoot, "sys", "fst.bin"))) return null;
try
{
ValidateExtractedGame(dataRoot, manifest);
return dataRoot;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
{
_reporter.Diagnostic(
"The installed game assets cannot be reused; the disc will be extracted again: " + ex.Message);
return null;
}
}
private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination,
PayloadManifest manifest, CancellationToken cancellationToken)
{
_reporter.Progress(InstallStages.ExtractDisc,
"Extracting the game disc. This is the longest preparation step...", 6);
// Extracted straight into a "DATA" subfolder so the on-disk layout matches what
// Installation.GameDataDirectory and every other reader of it already expect - nodtool
// itself has no such wrapper (it extracts sys/+files/ directly to whatever <outdir> is
// given), so this is purely destination-side, not a nodtool convention.
var dataRoot = Path.Combine(destination, "DATA");
var extraction = await ProcessRunner.RunAsync(nodTool,
["extract", Path.GetFullPath(gamePath), dataRoot, "-q"],
line => { if (!string.IsNullOrWhiteSpace(line)) _reporter.Diagnostic(line); },
cancellationToken);
if (extraction.ExitCode != 0)
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
ValidateExtractedGame(dataRoot, manifest);
}
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
{
var dol = Path.Combine(dataRoot, "sys", "main.dol");
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
var dolHash = InputValidation.Sha256File(dol);
var relHash = InputValidation.Sha256File(rel);
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
"Patched or otherwise modified game code cannot be installed safely.");
}
}
private static void ValidateInstallDirectory(string path) =>
FileSystemUtilities.EnsureUsableLocation(path, "The installation folder");
private static void EnsureSufficientRemainingDiskSpace(string installDirectory,
bool needsExtractedDisc, bool needsLocalBuild)
{
const long extractedDiscAllowance = 5L * 1024 * 1024 * 1024;
const long localBuildAllowance = 14L * 1024 * 1024 * 1024;
const long safetyAllowance = 2L * 1024 * 1024 * 1024;
// This preflight runs after the release payload and the compile-input snapshot have already
// been staged. Count only work that can still allocate space from this point.
var required = checked((needsExtractedDisc ? extractedDiscAllowance : 0) +
(needsLocalBuild ? localBuildAllowance : 0) + safetyAllowance);
FileSystemUtilities.EnsureFreeSpace(installDirectory, required, "Setup");
}
}