switch to standalone nod tool rather than dolphin-tool

This commit is contained in:
theofficialgman
2026-08-26 18:55:47 -04:00
parent edc5fa1dd3
commit f09590aa5d
18 changed files with 346 additions and 95 deletions
@@ -63,8 +63,8 @@ internal static class ConsoleCommands
{
using var payload = PayloadArchive.OpenCurrent();
var manifest = payload.ReadManifest();
var tool = Path.Combine(temp, "DolphinTool.exe");
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "DolphinTool.exe", tool);
var tool = Path.Combine(temp, "nodtool.exe");
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
+25 -10
View File
@@ -3,6 +3,7 @@ using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
using WiiCompiled.NodTool;
namespace WiiCompiled.Setup;
@@ -47,23 +48,37 @@ internal static class InputValidation
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
}
public static async Task<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
CancellationToken cancellationToken = default)
{
ValidateExtension(gamePath);
var result = await ProcessRunner.RunAsync(dolphinTool,
["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken);
var result = await ProcessRunner.RunAsync(nodTool,
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
if (result.ExitCode != 0)
throw new InvalidDataException("DolphinTool could not read this disc image. " + result.CombinedOutput.Trim());
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
var json = result.StandardOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(line => line.TrimStart().StartsWith('{'));
if (json is null)
throw new InvalidDataException("DolphinTool did not return disc metadata.");
return JsonSerializer.Deserialize<DiscHeader>(json)
?? throw new InvalidDataException("DolphinTool returned invalid disc metadata.");
var info = NodToolInfoParser.Parse(result.StandardOutput);
return new DiscHeader
{
GameId = info.GameId,
InternalName = info.Title,
Region = RegionFromGameId(info.GameId),
Revision = info.Revision,
};
}
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
? gameId[3] switch
{
'P' => "PAL",
'E' => "NTSC-U",
'J' => "NTSC-J",
'K' => "Korea",
'W' => "Taiwan",
_ => gameId[3].ToString(),
}
: "Unknown";
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
+16 -11
View File
@@ -57,9 +57,9 @@ internal sealed class InstallerEngine
var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing,
candidateRuntimeAssetsFingerprint, cancellationToken);
var installedDolphinTool = Path.Combine(existing.ToolkitDirectory, "DolphinTool.exe");
var installedNodTool = Path.Combine(existing.ToolkitDirectory, "nodtool.exe");
var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent,
File.Exists(installedDolphinTool));
File.Exists(installedNodTool));
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
_reporter.Progress(InstallStages.ExtractToolkit,
@@ -77,9 +77,9 @@ internal sealed class InstallerEngine
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
var dolphinTool = extractToolkit ? Path.Combine(toolkit, "DolphinTool.exe") : installedDolphinTool;
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
var header = await InputValidation.ReadDiscHeaderAsync(dolphinTool, options.GamePath,
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
@@ -153,7 +153,7 @@ internal sealed class InstallerEngine
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(dolphinTool, options.GamePath,
await ExtractGameAssetsAsync(nodTool, options.GamePath,
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
}
@@ -164,8 +164,8 @@ internal sealed class InstallerEngine
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
bool dolphinToolPresent) =>
!sameToolkit || !samePackageContent || !dolphinToolPresent;
bool nodToolPresent) =>
!sameToolkit || !samePackageContent || !nodToolPresent;
private static void AddComponent(List<InstallTransactionEntry> entries, string staging,
string installDirectory, string name) =>
@@ -469,18 +469,23 @@ internal sealed class InstallerEngine
}
}
private async Task ExtractGameAssetsAsync(string dolphinTool, string gamePath, string destination,
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);
var extraction = await ProcessRunner.RunAsync(dolphinTool,
["extract", "-i", Path.GetFullPath(gamePath), "-o", destination, "-g", "-q"],
// 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(Path.Combine(destination, "DATA"), manifest);
ValidateExtractedGame(dataRoot, manifest);
}
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
+7 -7
View File
@@ -143,17 +143,17 @@ internal static class SelfTests
private static void TestToolkitRefreshDecision()
{
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: false, samePackageContent: true,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("A republished workspace kept the installed toolkit; the shipped " +
"translator and project file could come from different releases.");
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: false,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("Changed toolkit package content was not extracted.");
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
dolphinToolPresent: false))
throw new Exception("A missing DolphinTool.exe did not force toolkit extraction.");
nodToolPresent: false))
throw new Exception("A missing nodtool.exe did not force toolkit extraction.");
if (InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("An unchanged toolkit was needlessly re-extracted.");
}
@@ -1010,7 +1010,7 @@ internal static class SelfTests
throw new Exception("The toolkit fingerprint is not stable.");
// A file that has nothing to do with generated code must not invalidate every install.
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "irrelevant");
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "irrelevant");
if (ToolkitFingerprint.Compute(root) != first)
throw new Exception("An unrelated toolkit file changed the fingerprint.");
@@ -1177,7 +1177,7 @@ internal static class SelfTests
"x86_64-w64-mingw32-clang++.exe", "x86_64-w64-mingw32-windres.exe"
})
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", executable), executable);
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}");
@@ -78,12 +78,12 @@ internal static class ToolkitFingerprint
var workspace = InstalledLayout.Workspace(root);
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
// DolphinTool validates/extracts the user disc but does not influence generated products.
// nodtool validates/extracts the user disc but does not influence generated products.
// Everything else in Toolkit can affect translation, compilation, linking, or copied
// runtime support and therefore belongs to the compile identity.
AddDirectory(entries, root, toolkit, null,
cancellationToken,
file => !Path.GetFileName(file).Equals("DolphinTool.exe", StringComparison.OrdinalIgnoreCase));
file => !Path.GetFileName(file).Equals("nodtool.exe", StringComparison.OrdinalIgnoreCase));
AddFile(entries, root, Path.Combine(workspace, "LocalBuild.ps1"), cancellationToken);
AddFile(entries, root, Path.Combine(workspace, "NativeBuildFlags.ps1"), cancellationToken);
AddDirectory(entries, root, Path.Combine(workspace, "projects"), null, cancellationToken);
@@ -14,4 +14,7 @@
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.NodTool\WiiCompiled.NodTool.csproj" />
</ItemGroup>
</Project>