diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 1acefe9d4..f6a8daa63 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -97,6 +97,24 @@ jobs: if-no-files-found: error archive: false + # The same setup, zipped with the recomplauncher.json RecompLauncher reads, so it unpacks into + # its own folder instead of being run as an installer. WiiCompiled-Setup.exe stays as it is. + - name: Zip for RecompLauncher + shell: pwsh + run: | + $stage = Join-Path $env:RUNNER_TEMP 'recomplauncher-zip' + New-Item -ItemType Directory -Force -Path $stage | Out-Null + Copy-Item Launcher/dist/WiiCompiled-Setup.exe $stage + Copy-Item Launcher/recomplauncher/windows.json (Join-Path $stage 'recomplauncher.json') + Compress-Archive -Path (Join-Path $stage '*') -DestinationPath Launcher/dist/WiiCompiled-Setup-windows-x64.zip + + - uses: actions/upload-artifact@v7 + with: + name: WiiCompiled-Setup-windows-x64-zip + path: Launcher/dist/WiiCompiled-Setup-windows-x64.zip + if-no-files-found: error + archive: false + macos-setup-package: name: macOS (universal Setup.pkg) runs-on: macos-15 @@ -307,7 +325,7 @@ jobs: set -euo pipefail ls -lR artifacts assets=() - for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg WiiCompiled-Setup-macos.run; do + for name in WiiCompiled-Setup.exe WiiCompiled-Setup-windows-x64.zip WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg WiiCompiled-Setup-macos.run; do found="$(find artifacts -type f -name "$name" | head -n 1)" [ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; } assets+=("$found") diff --git a/Launcher/WiiCompiled.Setup.Common/InstallProgress.cs b/Launcher/WiiCompiled.Setup.Common/InstallProgress.cs index 1650d98df..84895ff23 100644 --- a/Launcher/WiiCompiled.Setup.Common/InstallProgress.cs +++ b/Launcher/WiiCompiled.Setup.Common/InstallProgress.cs @@ -95,6 +95,40 @@ private static void WriteLine(object value) } } +/// +/// The --progress-lines format RecompLauncher reads from a setup step: one +/// progress <0 to 1> <text> line per update on stdout, diagnostics on stderr. The +/// outcome is the exit code. +/// +public sealed class LauncherProgressInstallReporter : IInstallReporter +{ + private readonly Action? _log; + private readonly object _gate = new(); + private int _lastPercent; + + public LauncherProgressInstallReporter(Action? log = null) => _log = log; + + public void Progress(string stage, string message, int percent) + { + lock (_gate) + { + // Monotonic, like the NDJSON reporter: the bar never walks backwards. + _lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99); + var text = message.ReplaceLineEndings(" ").Trim(); + Console.Out.WriteLine(string.Create(System.Globalization.CultureInfo.InvariantCulture, + $"progress {_lastPercent / 100.0:0.00} {text}")); + Console.Out.Flush(); + } + _log?.Invoke(message); + } + + public void Diagnostic(string line) + { + Console.Error.WriteLine(line); + _log?.Invoke(line); + } +} + /// Plain-text console reporting for a run without --progress-json. public sealed class ConsoleInstallReporter : IInstallReporter { diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs index 3eea85ba5..efe7ff825 100644 --- a/Launcher/WiiCompiled.Setup.Linux/Program.cs +++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs @@ -31,7 +31,15 @@ private static async Task RunAsync(string[] args, CancellationTokenSource c var (command, flags) = ParseArgs(args); if (command is null) { PrintUsage(); return 1; } var progressJson = flags.ContainsKey("progress-json"); - IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) : new ConsoleInstallReporter(); + var progressLines = flags.ContainsKey("progress-lines"); + if (progressJson && progressLines) + { + Console.Error.WriteLine("error: Choose only one of --progress-json and --progress-lines."); + return 1; + } + IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) + : progressLines ? (IInstallReporter)new LauncherProgressInstallReporter() + : new ConsoleInstallReporter(); try { @@ -361,7 +369,8 @@ install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR {--download-retro-wfc-payload | --skip-retro-wfc-payload}] [--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH] [--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH] - [--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR] + [--native-prebuilt-dir DIR] [--sysroot PATH] [--workspace DIR] + [--progress-json | --progress-lines] uninstall launch-base launch-retro diff --git a/Launcher/WiiCompiled.Setup.Windows/CommandLine.cs b/Launcher/WiiCompiled.Setup.Windows/CommandLine.cs index 14cd66409..4352a3e78 100644 --- a/Launcher/WiiCompiled.Setup.Windows/CommandLine.cs +++ b/Launcher/WiiCompiled.Setup.Windows/CommandLine.cs @@ -25,6 +25,7 @@ internal sealed record ModeRules { public string Flag { get; init; } = "this command"; public bool AcceptsProgressJson { get; init; } + public bool AcceptsProgressLines { get; init; } public bool AcceptsGame { get; init; } public bool RequiresGame { get; init; } public bool AcceptsRetroDirectory { get; init; } @@ -44,6 +45,7 @@ internal sealed class CommandLine public string? InstallDirectory { get; private set; } public bool Quiet { get; private set; } public bool ProgressJson { get; private set; } + public bool ProgressLines { get; private set; } public string? PayloadRootPath { get; private set; } public bool Portable { get; private set; } @@ -73,6 +75,7 @@ public static CommandLine Parse(string[] args) case "--payload-root": result.PayloadRootPath = RequireValue(args, ref i); break; case "--quiet": result.Quiet = true; break; case "--progress-json": result.ProgressJson = true; break; + case "--progress-lines": result.ProgressLines = true; break; case "--game": result.GamePath = RequireValue(args, ref i); break; case "--retro-dir": result.RetroDirectoryPath = RequireValue(args, ref i); break; case "--download-retro-wfc-payload": @@ -100,7 +103,7 @@ public static CommandLine Parse(string[] args) [AppMode.SilentInstall] = new ModeRules { Flag = "--silent", - AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true, + AcceptsProgressJson = true, AcceptsProgressLines = true, AcceptsGame = true, RequiresGame = true, AcceptsRetroDirectory = true, AcceptsPayloadMode = true, AcceptsPortable = true }, @@ -149,6 +152,9 @@ void Reject(bool present, string option) Reject(GamePath is not null && !rules.AcceptsGame, "--game"); Reject(RetroDirectoryPath is not null && !rules.AcceptsRetroDirectory, "--retro-dir"); Reject(ProgressJson && !rules.AcceptsProgressJson, "--progress-json"); + Reject(ProgressLines && !rules.AcceptsProgressLines, "--progress-lines"); + if (ProgressJson && ProgressLines) + throw new ArgumentException("Choose only one of --progress-json and --progress-lines."); Reject(PayloadRootPath is not null && !rules.RequiresPayloadRoot, "--payload-root"); Reject(Portable && !rules.AcceptsPortable, "--portable"); Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode, diff --git a/Launcher/WiiCompiled.Setup.Windows/ConsoleCommands.cs b/Launcher/WiiCompiled.Setup.Windows/ConsoleCommands.cs index 02ab9a2a2..6fd00d0c0 100644 --- a/Launcher/WiiCompiled.Setup.Windows/ConsoleCommands.cs +++ b/Launcher/WiiCompiled.Setup.Windows/ConsoleCommands.cs @@ -12,6 +12,7 @@ public static int Help() Console.Out.WriteLine(); Console.Out.WriteLine("Commands:"); Console.Out.WriteLine(" --silent --game --install-dir [--retro-dir ] [--portable]"); + Console.Out.WriteLine(" [--progress-json | --progress-lines]"); Console.Out.WriteLine(" --verify-inputs --game [--retro-dir ]"); Console.Out.WriteLine(" --check-products [--install-dir ] [--retro-dir ] [--progress-json]"); Console.Out.WriteLine(" --repair-products --install-dir --retro-dir " + @@ -106,7 +107,9 @@ void Log(string message) File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}"); var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null; - IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log); + IInstallReporter reporter = ndjson ?? (command.ProgressLines + ? (IInstallReporter)new LauncherProgressInstallReporter(Log) + : new ConsoleInstallReporter(Log)); var installDirectory = Path.GetFullPath(command.InstallDirectory!); try { diff --git a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs index f688a8acd..dcffe7ed4 100644 --- a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs +++ b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs @@ -543,6 +543,16 @@ void Rejects(string description, params string[] args) if (portable.Mode != AppMode.SilentInstall || !portable.Portable) throw new Exception("The portable install contract did not parse."); + var lines = CommandLine.Parse([ + "--silent", "--game", "game.iso", "--install-dir", "D:\\Games\\Recomp\\Install", + "--portable", "--progress-lines" + ]); + if (lines.Mode != AppMode.SilentInstall || !lines.ProgressLines || lines.ProgressJson) + throw new Exception("The progress-lines install contract did not parse."); + Rejects("--progress-lines with --progress-json", "--silent", "--game", "g.iso", + "--progress-lines", "--progress-json"); + Rejects("--progress-lines outside an install", "--check-products", "--progress-lines"); + Rejects("--portable without an install directory", "--silent", "--game", "g.iso", "--portable"); Rejects("--portable outside an install", "--check-products", "--install-dir", "fixed", "--portable"); // The config verbs are gone: WheelWizard reads and writes Config.toml directly now. diff --git a/Launcher/build-appimage.sh b/Launcher/build-appimage.sh index 2f6b2bbaa..cf5df2d17 100755 --- a/Launcher/build-appimage.sh +++ b/Launcher/build-appimage.sh @@ -263,6 +263,9 @@ exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \ APPRUN chmod +x "$appdir/AppRun" +# RecompLauncher reads this straight out of the AppImage to set the game up and start it. +cp "$script_dir/recomplauncher/linux.json" "$appdir/recomplauncher.json" + echo "Writing desktop entry and icon..." cat > "$appdir/wiicompiled-setup.desktop" <<'DESKTOP' [Desktop Entry] diff --git a/Launcher/recomplauncher/linux.json b/Launcher/recomplauncher/linux.json new file mode 100644 index 000000000..8ebab1476 --- /dev/null +++ b/Launcher/recomplauncher/linux.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://recomplauncher.com/schema/recomplauncher.json", + "format": 1, + "name": "Mario Kart WiiCompiled", + "repo": "patchzyy/Wiicompiled", + "game": { + "kind": "iso", + "label": "Your own disc image of Mario Kart Wii: the PAL (RMCP01), USA (RMCE01) or Japan (RMCJ01) release, a clean dump as .iso, .gcm, .gcz, .ciso, .wbfs, .wia or .rvz.", + "check": { "ids": ["RMCP01", "RMCE01", "RMCJ01"] } + }, + "prepare": { + "program": ["WiiCompiled-Setup-x86_64.AppImage", "WiiCompiled-Setup-aarch64.AppImage"], + "args": ["install", "--game", "{game}", "--progress-lines"], + "label": "Translating and compiling the game from your disc. This takes a while, and only happens once", + "timeout": 10800 + }, + "start": { + "program": ["WiiCompiled-Setup-x86_64.AppImage", "WiiCompiled-Setup-aarch64.AppImage"], + "args": ["launch-base"] + }, + "config": [ + { + "id": "main", + "format": "toml", + "file": [{ "path": "{local}/WiiCompiled/Config.toml" }] + } + ], + "saves": ["{local}/WiiCompiled/NAND"], + "folders": { + "saves": [{ "path": "{local}/WiiCompiled/NAND" }], + "logs": [{ "path": "{local}/WiiCompiled/Logs" }] + }, + "settings": [ + { + "id": "display_mode", "label": "Window", "group": "Video", + "help": "Windowed, borderless fullscreen, or exclusive fullscreen, which asks your screen for the closest mode to the game's frame rate.", + "choices": [ + { "label": "Windowed", "set": { "video/display_mode": "\"windowed\"" } }, + { "label": "Borderless", "set": { "video/display_mode": "\"borderless\"" } }, + { "label": "Exclusive", "set": { "video/display_mode": "\"exclusive\"" } } + ], + "defaults": { "video/display_mode": "\"windowed\"" } + }, + { + "id": "resolution", "label": "Resolution", "group": "Video", + "help": "What the game draws at. Native is the Wii's own. Auto follows the size of the window. Higher is sharper and needs a faster graphics card.", + "choices": [ + { "label": "Auto", "set": { "video/resolution_multiplier": "0.0" } }, + { "label": "Native", "set": { "video/resolution_multiplier": "1.0" } }, + { "label": "1.5x", "set": { "video/resolution_multiplier": "1.5" } }, + { "label": "2x", "set": { "video/resolution_multiplier": "2.0" } }, + { "label": "3x", "set": { "video/resolution_multiplier": "3.0" } }, + { "label": "4x", "set": { "video/resolution_multiplier": "4.0" } }, + { "label": "6x", "set": { "video/resolution_multiplier": "6.0" } }, + { "label": "8x", "set": { "video/resolution_multiplier": "8.0" } } + ], + "defaults": { "video/resolution_multiplier": "1.0" } + }, + { + "id": "aspect", "label": "Aspect ratio", "group": "Video", + "help": "16:9 for widescreen screens, 4:3 for the original shape. Takes effect the next time the game starts.", + "choices": [ + { "label": "16:9", "set": { "video/widescreen": "true" } }, + { "label": "4:3", "set": { "video/widescreen": "false" } } + ], + "defaults": { "video/widescreen": "true" } + }, + { + "id": "interpolation", "label": "Race frame rate", "group": "Video", + "help": "The game runs at 60 FPS. Interpolation adds frames in between during races for 120 or 144 Hz screens. It is experimental and can show artifacts.", + "choices": [ + { "label": "60 FPS", "set": { "video/frame_interpolation_fps": "0" } }, + { "label": "120 FPS", "set": { "video/frame_interpolation_fps": "120" } }, + { "label": "180 FPS", "set": { "video/frame_interpolation_fps": "180" } } + ], + "defaults": { "video/frame_interpolation_fps": "0" } + }, + { + "id": "vsync", "label": "V-Sync", "group": "Video", + "help": "Waits for the screen's refresh. Turn it on if frame interpolation looks uneven on your screen.", + "choices": [ + { "label": "Off", "set": { "video/vsync": "false" } }, + { "label": "On", "set": { "video/vsync": "true" } } + ], + "defaults": { "video/vsync": "false" } + }, + { + "id": "fps", "label": "Show FPS", "group": "Video", + "help": "The frame rate in a corner of the game.", + "choices": [ + { "label": "On", "set": { "video/show_fps": "true" } }, + { "label": "Off", "set": { "video/show_fps": "false" } } + ], + "defaults": { "video/show_fps": "true" } + }, + { + "id": "mute", "label": "Sound", "group": "Audio", + "help": "Turn all of the game's sound off or on.", + "choices": [ + { "label": "On", "set": { "audio/muted": "false" } }, + { "label": "Muted", "set": { "audio/muted": "true" } } + ], + "defaults": { "audio/muted": "false" } + }, + { + "id": "ducking", "label": "Music while other audio plays", "group": "Audio", + "help": "Mutes the game's music while another app, like a video or a music player, is playing sound.", + "choices": [ + { "label": "Keep playing", "set": { "audio/attenuate_music_when_media_plays": "false" } }, + { "label": "Mute music", "set": { "audio/attenuate_music_when_media_plays": "true" } } + ], + "defaults": { "audio/attenuate_music_when_media_plays": "false" } + } + ], + "actions": [ + { "id": "saves", "label": "Saves", "help": "Your Mario Kart Wii save data.", "open": "saves" }, + { "id": "logs", "label": "Logs", "help": "The log of each run, for bug reports.", "open": "logs" } + ] +} diff --git a/Launcher/recomplauncher/windows.json b/Launcher/recomplauncher/windows.json new file mode 100644 index 000000000..43fb64dd0 --- /dev/null +++ b/Launcher/recomplauncher/windows.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://recomplauncher.com/schema/recomplauncher.json", + "format": 1, + "name": "Mario Kart WiiCompiled", + "repo": "patchzyy/Wiicompiled", + "game": { + "kind": "iso", + "label": "Your own disc image of Mario Kart Wii: the PAL (RMCP01), USA (RMCE01) or Japan (RMCJ01) release, a clean dump as .iso, .gcm, .gcz, .ciso, .wbfs, .wia or .rvz.", + "check": { "ids": ["RMCP01", "RMCE01", "RMCJ01"] } + }, + "prepare": { + "program": ["WiiCompiled-Setup.exe"], + "args": ["--silent", "--game", "{game}", "--install-dir", "{install}/WiiCompiled/Install", "--portable", "--progress-lines"], + "label": "Translating and compiling the game from your disc. This takes a while, and only happens once", + "timeout": 10800 + }, + "start": { + "program": ["WiiCompiled/Install/WiiCompiled-Setup.exe"], + "args": ["--launch-base"] + }, + "config": [ + { + "id": "main", + "format": "toml", + "file": [{ "path": "{install}/WiiCompiled/UserData/Config.toml" }] + } + ], + "saves": ["{install}/WiiCompiled/UserData/NAND"], + "folders": { + "saves": [{ "path": "{install}/WiiCompiled/UserData/NAND" }], + "logs": [{ "path": "{install}/WiiCompiled/UserData/Logs" }] + }, + "settings": [ + { + "id": "display_mode", "label": "Window", "group": "Video", + "help": "Windowed, borderless fullscreen, or exclusive fullscreen, which asks your screen for the closest mode to the game's frame rate.", + "choices": [ + { "label": "Windowed", "set": { "video/display_mode": "\"windowed\"" } }, + { "label": "Borderless", "set": { "video/display_mode": "\"borderless\"" } }, + { "label": "Exclusive", "set": { "video/display_mode": "\"exclusive\"" } } + ], + "defaults": { "video/display_mode": "\"windowed\"" } + }, + { + "id": "resolution", "label": "Resolution", "group": "Video", + "help": "What the game draws at. Native is the Wii's own. Auto follows the size of the window. Higher is sharper and needs a faster graphics card.", + "choices": [ + { "label": "Auto", "set": { "video/resolution_multiplier": "0.0" } }, + { "label": "Native", "set": { "video/resolution_multiplier": "1.0" } }, + { "label": "1.5x", "set": { "video/resolution_multiplier": "1.5" } }, + { "label": "2x", "set": { "video/resolution_multiplier": "2.0" } }, + { "label": "3x", "set": { "video/resolution_multiplier": "3.0" } }, + { "label": "4x", "set": { "video/resolution_multiplier": "4.0" } }, + { "label": "6x", "set": { "video/resolution_multiplier": "6.0" } }, + { "label": "8x", "set": { "video/resolution_multiplier": "8.0" } } + ], + "defaults": { "video/resolution_multiplier": "1.0" } + }, + { + "id": "aspect", "label": "Aspect ratio", "group": "Video", + "help": "16:9 for widescreen screens, 4:3 for the original shape. Takes effect the next time the game starts.", + "choices": [ + { "label": "16:9", "set": { "video/widescreen": "true" } }, + { "label": "4:3", "set": { "video/widescreen": "false" } } + ], + "defaults": { "video/widescreen": "true" } + }, + { + "id": "interpolation", "label": "Race frame rate", "group": "Video", + "help": "The game runs at 60 FPS. Interpolation adds frames in between during races for 120 or 144 Hz screens. It is experimental and can show artifacts.", + "choices": [ + { "label": "60 FPS", "set": { "video/frame_interpolation_fps": "0" } }, + { "label": "120 FPS", "set": { "video/frame_interpolation_fps": "120" } }, + { "label": "180 FPS", "set": { "video/frame_interpolation_fps": "180" } } + ], + "defaults": { "video/frame_interpolation_fps": "0" } + }, + { + "id": "vsync", "label": "V-Sync", "group": "Video", + "help": "Waits for the screen's refresh. Turn it on if frame interpolation looks uneven on your screen.", + "choices": [ + { "label": "Off", "set": { "video/vsync": "false" } }, + { "label": "On", "set": { "video/vsync": "true" } } + ], + "defaults": { "video/vsync": "false" } + }, + { + "id": "fps", "label": "Show FPS", "group": "Video", + "help": "The frame rate in a corner of the game.", + "choices": [ + { "label": "On", "set": { "video/show_fps": "true" } }, + { "label": "Off", "set": { "video/show_fps": "false" } } + ], + "defaults": { "video/show_fps": "true" } + }, + { + "id": "mute", "label": "Sound", "group": "Audio", + "help": "Turn all of the game's sound off or on.", + "choices": [ + { "label": "On", "set": { "audio/muted": "false" } }, + { "label": "Muted", "set": { "audio/muted": "true" } } + ], + "defaults": { "audio/muted": "false" } + }, + { + "id": "ducking", "label": "Music while other audio plays", "group": "Audio", + "help": "Mutes the game's music while another app, like a video or a music player, is playing sound.", + "choices": [ + { "label": "Keep playing", "set": { "audio/attenuate_music_when_media_plays": "false" } }, + { "label": "Mute music", "set": { "audio/attenuate_music_when_media_plays": "true" } } + ], + "defaults": { "audio/attenuate_music_when_media_plays": "false" } + } + ], + "actions": [ + { "id": "saves", "label": "Saves", "help": "Your Mario Kart Wii save data.", "open": "saves" }, + { "id": "logs", "label": "Logs", "help": "The log of each run, for bug reports.", "open": "logs" } + ] +} diff --git a/README.md b/README.md index 7c31b9029..d54b7df0b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Total downloads License: GPLv3

+

+ Get it on RecompLauncher +

A native PC port of Mario Kart Wii, made with static recompilation.