NTSC-U and NTSC-J support (#326)

* add ntsc-u and ntsc-j support

Co-authored-by: DarthMDev <mgracer48@yahoo.com>
Co-authored-by: rooklz <rookieb@muzo-works.com>

* hash inc files and require project for shards

* port remaining stale rr us hooks

* drop map report generation

* drop one-off region porters

---------

Co-authored-by: DarthMDev <mgracer48@yahoo.com>
Co-authored-by: rooklz <rookieb@muzo-works.com>
This commit is contained in:
patchzyy
2026-10-21 17:37:44 +02:00
committed by GitHub
parent ad95c5c7da
commit e9f1fdd77c
95 changed files with 64733 additions and 646 deletions
@@ -40,6 +40,28 @@ internal sealed class InstallerEngine
var toolkit = InstalledLayout.Toolkit(staging);
var workspace = InstalledLayout.Workspace(staging);
// The disc decides which release's inputs everything below expects, so it is identified
// before any reuse decision or toolkit extraction.
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 1);
var nodTool = payload.ExtractDiscTool(Path.Combine(staging, "disc-tool"));
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
var release = InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
? null
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
ValidateRetroOptions(canonicalRetroRoot, options.RetroWfcPayloadMode);
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}.");
}
var candidateToolkitFingerprint = manifest.ToolkitFingerprint;
var candidateToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint;
var candidateRuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint;
@@ -49,7 +71,7 @@ internal sealed class InstallerEngine
var installedToolkitPackageFingerprint = existing.IsPresent
? existing.ResolveToolkitPackageFingerprint()
: "";
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
var reusableGameAssets = FindReusableGameAssets(existing, release);
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
@@ -68,7 +90,7 @@ internal sealed class InstallerEngine
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
_reporter.Progress(InstallStages.ExtractToolkit,
"Inspecting the release's local recompilation toolkit...", 1);
"Inspecting the release's local recompilation toolkit...", 4);
if (extractToolkit) payload.ExtractDirectory(InstalledLayout.ToolkitDirectoryName, toolkit);
if (extractWorkspace)
{
@@ -82,30 +104,10 @@ internal sealed class InstallerEngine
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);
candidateToolkitFingerprint, release.DolSha256, release.RelSha256);
if (desiredInputDrift.Retro && retroCompileInputs is null)
throw new InvalidOperationException(
"This release changes the clean game inputs used by the installed Retro Rewind product. " +
@@ -113,7 +115,7 @@ internal sealed class InstallerEngine
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, Path.Combine(staging, "repair"),
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
release.DolSha256, release.RelSha256, cancellationToken);
var remainingCancellation = reconciliation.PublicationCommitted
? CancellationToken.None
: cancellationToken;
@@ -125,11 +127,10 @@ internal sealed class InstallerEngine
remainingCancellation.ThrowIfCancellationRequested();
EnsureReconciliationCurrent(existing, candidateToolkitFingerprint, retroCompileInputs,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
release.DolSha256, release.RelSha256, reconciliation.CachedRetroWfcPayloadMatches);
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
release, retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, updatedState);
remainingCancellation.ThrowIfCancellationRequested();
@@ -159,11 +160,11 @@ internal sealed class InstallerEngine
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(nodTool, options.GamePath,
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
Path.Combine(staging, "GameAssets"), release, cancellationToken);
}
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
previousState, options, canonicalRetroRoot, retroCompileInputs,
release, previousState, options, canonicalRetroRoot, retroCompileInputs,
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
cancellationToken: cancellationToken);
}
@@ -187,7 +188,7 @@ internal sealed class InstallerEngine
["generated", "build", "native-build", "Assets", "PulsarPacks"];
private async Task PublishToolkitAndReconcileProductsAsync(Installation existing, string staging,
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
string stagedWorkspace, PayloadManifest manifest, GameRelease release, InstallState? previousState,
InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
@@ -206,7 +207,7 @@ internal sealed class InstallerEngine
}
var state = BuildInstallState(previousState, installDirectory, manifest,
var state = BuildInstallState(previousState, installDirectory, manifest, release,
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
PrepareMetadata(staging, manifest, state);
@@ -226,10 +227,9 @@ internal sealed class InstallerEngine
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
canonicalRetroRoot, options.RetroWfcPayloadMode, repairScratch: null,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
release.DolSha256, release.RelSha256, cancellationToken);
EnsureReconciliationCurrent(existing, manifest.ToolkitFingerprint, retroCompileInputs,
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
reconciliation.CachedRetroWfcPayloadMatches);
release.DolSha256, release.RelSha256, reconciliation.CachedRetroWfcPayloadMatches);
}
private Task<ProductRepairService.ReconciliationResult> ReconcileSameToolkitAsync(Installation existing,
@@ -354,7 +354,8 @@ internal sealed class InstallerEngine
}
private static InstallState BuildInstallState(InstallState? previousState, string installDirectory,
PayloadManifest manifest, RetroRewindCompileInputs? compileInputs, string? canonicalRetroRoot,
PayloadManifest manifest, GameRelease release, RetroRewindCompileInputs? compileInputs,
string? canonicalRetroRoot,
RetroWfcPayloadMode payloadMode,
RetroWfcPayloadSnapshot? retroWfcPayloadSnapshot = null)
{
@@ -368,8 +369,8 @@ internal sealed class InstallerEngine
InstalledUtc = previousState?.InstalledUtc ?? DateTime.UtcNow,
RetroRewindInstalled = previousState?.RetroRewindInstalled ?? false,
ToolkitReleaseTag = manifest.ToolkitReleaseTag,
DolSha256 = manifest.ExpectedDolSha256,
RelSha256 = manifest.ExpectedRelSha256,
DolSha256 = release.DolSha256,
RelSha256 = release.RelSha256,
RetroRewindCodePulSha256 = previousState?.RetroRewindCodePulSha256 ?? "",
RetroRewindCompileInputsSha256 = previousState?.RetroRewindCompileInputsSha256 ?? "",
RetroWfcPayloadMode = previousState?.RetroWfcPayloadMode ?? "",
@@ -444,8 +445,7 @@ internal sealed class InstallerEngine
Path.Combine(InstalledLayout.Workspace(installDirectory), "runtime", "assets")));
}
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode,
PayloadManifest manifest)
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode)
{
if (canonicalRetroRoot is null && mode != RetroWfcPayloadMode.NotApplicable)
throw new InvalidOperationException(
@@ -453,11 +453,9 @@ internal sealed class InstallerEngine
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)
private string? FindReusableGameAssets(Installation existing, GameRelease release)
{
var dataRoot = existing.GameDataDirectory;
// No fst.bin simply means nothing was extracted here before; anything past that gate is an
@@ -466,7 +464,7 @@ internal sealed class InstallerEngine
if (!File.Exists(Path.Combine(dataRoot, "sys", "fst.bin"))) return null;
try
{
ValidateExtractedGame(dataRoot, manifest);
ValidateExtractedGame(dataRoot, release);
return dataRoot;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
@@ -478,7 +476,7 @@ internal sealed class InstallerEngine
}
private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination,
PayloadManifest manifest, CancellationToken cancellationToken)
GameRelease release, CancellationToken cancellationToken)
{
_reporter.Progress(InstallStages.ExtractDisc,
"Extracting the game disc. This is the longest preparation step...", 6);
@@ -493,15 +491,16 @@ internal sealed class InstallerEngine
cancellationToken);
if (extraction.ExitCode != 0)
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
ValidateExtractedGame(dataRoot, manifest);
ValidateExtractedGame(dataRoot, release);
}
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
internal static void ValidateExtractedGame(string dataRoot, GameRelease release)
{
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");
var missing = new[] { dol, rel, fst }.Where(path => !File.Exists(path))
var boot = Path.Combine(dataRoot, "sys", "boot.bin");
var missing = new[] { dol, rel, fst, boot }.Where(path => !File.Exists(path))
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
if (missing.Count > 0)
throw new InvalidDataException(
@@ -509,10 +508,13 @@ internal sealed class InstallerEngine
string.Join(", ", missing) + ".");
var mismatches = new List<string>();
var discGameId = Installation.ReadDiscGameId(dataRoot);
if (!discGameId.Equals(release.GameId, StringComparison.Ordinal))
mismatches.Add($"sys/boot.bin names {discGameId}");
foreach (var (name, path, expected) in new[]
{
("main.dol", dol, manifest.ExpectedDolSha256),
("StaticR.rel", rel, manifest.ExpectedRelSha256)
("main.dol", dol, release.DolSha256),
("StaticR.rel", rel, release.RelSha256)
})
{
var actual = InputValidation.Sha256File(path);
@@ -523,7 +525,7 @@ internal sealed class InstallerEngine
if (mismatches.Count > 0)
throw new InvalidDataException(
"The disc is RMCP01 but its game code does not match the supported clean PAL revision: " +
$"The disc is {release.GameId} but its game data does not match the supported clean revision: " +
string.Join("; ", mismatches) + ". " +
"Patched or otherwise modified game code cannot be installed safely.");
}