diff --git a/WheelWizard.Test/Features/Recomp/RecompLinuxTests.cs b/WheelWizard.Test/Features/Recomp/RecompLinuxTests.cs index 6d1587518..7eba17a24 100644 --- a/WheelWizard.Test/Features/Recomp/RecompLinuxTests.cs +++ b/WheelWizard.Test/Features/Recomp/RecompLinuxTests.cs @@ -78,6 +78,7 @@ public void ReleaseResolver_PicksTheAssetItIsAskedFor() new GithubAsset { Name = "WiiCompiled-Setup.exe", BrowserDownloadUrl = "https://example.com/exe" }, new GithubAsset { Name = "WiiCompiled-Setup-x86_64.AppImage", BrowserDownloadUrl = "https://example.com/x86_64" }, new GithubAsset { Name = "WiiCompiled-Setup-aarch64.AppImage", BrowserDownloadUrl = "https://example.com/aarch64" }, + new GithubAsset { Name = RecompPlatform.MacReleaseAssetName, BrowserDownloadUrl = "https://example.com/macos" }, ], }; var windowsOnly = new GithubRelease @@ -93,6 +94,12 @@ public void ReleaseResolver_PicksTheAssetItIsAskedFor() Assert.Equal("v0.3.0", RecompReleaseResolver.FindLatest([release, windowsOnly], "WiiCompiled-Setup.exe")?.TagName); Assert.Null(RecompReleaseResolver.FindLatest([windowsOnly], "WiiCompiled-Setup-aarch64.AppImage")); + + Assert.Equal( + "https://example.com/macos", + RecompReleaseResolver.FindLatest([release, windowsOnly], RecompPlatform.MacReleaseAssetName)?.SetupDownloadUrl + ); + Assert.Null(RecompReleaseResolver.FindLatest([windowsOnly], RecompPlatform.MacReleaseAssetName)); } [Fact] diff --git a/WheelWizard.Test/Features/Recomp/RecompPathsTests.cs b/WheelWizard.Test/Features/Recomp/RecompPathsTests.cs index cc0619e39..a25306ebe 100644 --- a/WheelWizard.Test/Features/Recomp/RecompPathsTests.cs +++ b/WheelWizard.Test/Features/Recomp/RecompPathsTests.cs @@ -60,4 +60,23 @@ public void LinuxLayout_LeavesBackendStateInXdgData_WhenLauncherDataMoves(Archit Assert.Equal("/relocated/Recomp/Install/install-state.json", paths.InstallStateFilePath); Assert.Equal("/relocated/Recomp/Nand", paths.NandCopyFolderPath); } + + [Fact] + public void MacLayout_KeepsBackendStateInApplicationSupport_AndInstallsTheDownloadedRun() + { + var fs = new MockFileSystem(options => options.SimulatingOperatingSystem(SimulationMode.MacOS)); + var location = Substitute.For(); + location.DirectoryPath.Returns("/first"); + var environment = Substitute.For(); + environment.IsMacOS.Returns(true); + environment.OSArchitecture.Returns(Architecture.Arm64); + environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).Returns("/support"); + var paths = new RecompPaths(location, fs, environment); + + Assert.False(paths.IsPortableInstall); + Assert.Equal("/support/WiiCompiled/Config.toml", paths.ConfigFilePath); + Assert.Equal("/support/WiiCompiled/NAND", paths.PrivateNandFolderPath); + Assert.Equal("/support/WiiCompiled/install-state.json", paths.LinuxBackendStateFilePath); + Assert.Equal("/first/Recomp/Install/WiiCompiled-Setup-macos.run", paths.SetupFilePath); + } } diff --git a/WheelWizard/Features/Recomp/RecompExtensions.cs b/WheelWizard/Features/Recomp/RecompExtensions.cs index 68345ecfa..022ba68e2 100644 --- a/WheelWizard/Features/Recomp/RecompExtensions.cs +++ b/WheelWizard/Features/Recomp/RecompExtensions.cs @@ -8,10 +8,10 @@ public static class RecompExtensions { /// /// Registers the Mario Kart Wii recomp frontend. - /// The recomp only ships for Windows and Linux, so on every other platform nothing is registered at - /// all; ISettingsManager.IsRecompModeActive() is false there, so nothing ever resolves these. - /// Both platforms share how a setup release is found and downloaded; what that setup is then told to - /// do differs, so each gets its own environment and install service. + /// The recomp only runs on Windows, Linux and macOS, so on every other platform nothing is registered + /// at all; ISettingsManager.IsRecompModeActive() is false elsewhere, so nothing ever resolves + /// these. Windows uses the v1 setup contract; Linux and macOS share the subcommand setup, so they share + /// an environment and install service. /// public static IServiceCollection AddRecomp(this IServiceCollection services) { @@ -38,7 +38,7 @@ public static IServiceCollection AddRecomp(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - if (RecompPlatform.IsLinux) + if (RecompPlatform.UsesSubcommandSetup) { services.AddSingleton(); services.AddSingleton(); diff --git a/WheelWizard/Features/Recomp/RecompLinuxInstallService.cs b/WheelWizard/Features/Recomp/RecompLinuxInstallService.cs index 790325488..16f32449c 100644 --- a/WheelWizard/Features/Recomp/RecompLinuxInstallService.cs +++ b/WheelWizard/Features/Recomp/RecompLinuxInstallService.cs @@ -222,11 +222,17 @@ public async Task InstallAsync( } /// - /// Decides based on the setup file path whether or not this is a bundled recomp setup binary + /// Decides based on the setup file path whether or not this is a bundled recomp setup binary. A + /// downloaded setup (the Linux AppImage or the macOS .run) shares the extension of this platform's + /// release asset; a bundled one, such as the Flatpak extension's wrapper, does not. /// private bool IsBundledRecomp(string setupFilePath) { - return !setupFilePath.EndsWith(".AppImage", StringComparison.OrdinalIgnoreCase); + return !string.Equals( + fileSystem.Path.GetExtension(setupFilePath), + fileSystem.Path.GetExtension(RecompPlatform.SetupFileName), + StringComparison.OrdinalIgnoreCase + ); } private async Task InstallCoreAsync( diff --git a/WheelWizard/Features/Recomp/RecompPaths.cs b/WheelWizard/Features/Recomp/RecompPaths.cs index 481bcdcc4..d1efd5ba1 100644 --- a/WheelWizard/Features/Recomp/RecompPaths.cs +++ b/WheelWizard/Features/Recomp/RecompPaths.cs @@ -27,22 +27,31 @@ public sealed class RecompPaths(IApplicationDataLocation applicationData, IFileS { public const string InstallStateFileName = "install-state.json"; private bool IsLinux => environment.IsLinux && RecompPlatform.LinuxReleaseAssetName(environment.OSArchitecture) is not null; + private bool IsMacOS => environment.IsMacOS; + + // Linux and macOS both run a setup that keeps its products, Config.toml and NAND in its own backend folder. + private bool UsesBackendFolder => IsLinux || IsMacOS; private bool IsFlatpak => environment.IsFlatpakSandboxed(fileSystem); public string RootFolderPath => fileSystem.Path.Combine(applicationData.DirectoryPath, "Recomp"); public string InstallFolderPath => fileSystem.Path.Combine(RootFolderPath, "Install"); - public bool IsPortableInstall => !IsLinux; + public bool IsPortableInstall => !UsesBackendFolder; public string CacheFolderPath => fileSystem.Path.Combine(RootFolderPath, "Cache"); public string UserDataFolderPath => fileSystem.Path.Combine(RootFolderPath, "UserData"); public string InstallStateFilePath => fileSystem.Path.Combine(InstallFolderPath, InstallStateFileName); public string SetupFilePath => IsFlatpak ? "/app/extensions/backends/wiicompiled/bin/wiicompiled-setup" - : fileSystem.Path.Combine(InstallFolderPath, IsLinux ? "WiiCompiled-Setup.AppImage" : RecompSetupCommandBuilder.SetupFileName); + : fileSystem.Path.Combine( + InstallFolderPath, + IsLinux ? "WiiCompiled-Setup.AppImage" + : IsMacOS ? RecompPlatform.MacReleaseAssetName + : RecompSetupCommandBuilder.SetupFileName + ); public string LinuxBackendFolderPath => fileSystem.Path.Combine(environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled"); public string LinuxBackendStateFilePath => fileSystem.Path.Combine(LinuxBackendFolderPath, InstallStateFileName); - public string ConfigFilePath => fileSystem.Path.Combine(IsLinux ? LinuxBackendFolderPath : UserDataFolderPath, "Config.toml"); + public string ConfigFilePath => fileSystem.Path.Combine(UsesBackendFolder ? LinuxBackendFolderPath : UserDataFolderPath, "Config.toml"); public string NandCopyFolderPath => fileSystem.Path.Combine(RootFolderPath, "Nand"); public string PortableMarkerFilePath => fileSystem.Path.Combine(RootFolderPath, "portable.txt"); - public string PrivateNandFolderPath => fileSystem.Path.Combine(IsLinux ? LinuxBackendFolderPath : UserDataFolderPath, "NAND"); + public string PrivateNandFolderPath => fileSystem.Path.Combine(UsesBackendFolder ? LinuxBackendFolderPath : UserDataFolderPath, "NAND"); } diff --git a/WheelWizard/Features/Recomp/RecompPlatform.cs b/WheelWizard/Features/Recomp/RecompPlatform.cs index 9414e866e..d096299c1 100644 --- a/WheelWizard/Features/Recomp/RecompPlatform.cs +++ b/WheelWizard/Features/Recomp/RecompPlatform.cs @@ -5,9 +5,10 @@ namespace WheelWizard.Recomp; /// /// Which platforms the WiiCompiled integration runs on, and which setup asset each of them downloads. /// Windows drives WiiCompiled-Setup.exe through the v1 command-line contract. Linux drives the -/// AppImage through its own subcommand interface (see ). Every -/// other platform is unsupported, so ISettingsManager.IsRecompModeActive() is false there and -/// nothing recomp-related is ever registered or shown. +/// AppImage through its own subcommand interface (see ), and macOS +/// drives the self-extracting .run with that same interface. Every other platform is unsupported, so +/// ISettingsManager.IsRecompModeActive() is false elsewhere and nothing recomp-related is ever +/// registered or shown. /// public static class RecompPlatform { @@ -18,14 +19,28 @@ public static class RecompPlatform /// public static bool IsLinux { get; } = OperatingSystem.IsLinux() && LinuxReleaseAssetName(RuntimeInformation.OSArchitecture) is not null; - public static bool IsSupported => OperatingSystem.IsWindows() || IsLinux; + /// + /// Whether this is a macOS build. macOS downloads , a self-extracting + /// setup for both architectures that speaks the same subcommand interface as the Linux AppImage. + /// + public static bool IsMacOS { get; } = OperatingSystem.IsMacOS(); + + /// Whether this platform drives the setup through its subcommand interface (Linux and macOS). + public static bool UsesSubcommandSetup => IsLinux || IsMacOS; + + public static bool IsSupported => OperatingSystem.IsWindows() || UsesSubcommandSetup; + + /// The macOS release asset, fixed by the recomp's packaging workflow. It is also the installed host's name. + public const string MacReleaseAssetName = "WiiCompiled-Setup-macos.run"; /// The name of the installed host copy inside Wheel Wizard's recomp install directory. - public static string SetupFileName => IsLinux ? "WiiCompiled-Setup.AppImage" : RecompSetupCommandBuilder.SetupFileName; + public static string SetupFileName => + IsLinux ? "WiiCompiled-Setup.AppImage" + : IsMacOS ? MacReleaseAssetName + : RecompSetupCommandBuilder.SetupFileName; /// The release asset to download on this machine. - public static string ReleaseAssetName => - IsLinux ? LinuxReleaseAssetName(RuntimeInformation.OSArchitecture)! : RecompSetupCommandBuilder.SetupFileName; + public static string ReleaseAssetName => IsLinux ? LinuxReleaseAssetName(RuntimeInformation.OSArchitecture)! : SetupFileName; /// /// The AppImage the recomp publishes for a Linux architecture, or when it diff --git a/WheelWizard/Features/Recomp/RecompVideoConfig.cs b/WheelWizard/Features/Recomp/RecompVideoConfig.cs index b90b019e1..f5398d412 100644 --- a/WheelWizard/Features/Recomp/RecompVideoConfig.cs +++ b/WheelWizard/Features/Recomp/RecompVideoConfig.cs @@ -12,10 +12,13 @@ public static class RecompVideoConfig /// /// The graphics APIs WheelWizard offers, in the order they are shown. The backend enumerates more /// (auto, d3d11, opengl, ...), but only these actually run the game reliably, so nothing else is - /// ever offered; Linux has no DirectX, so only Vulkan remains there. A value outside this list - /// stays untouched until the user picks one of these. + /// ever offered; Linux has no DirectX, so only Vulkan remains there, and macOS only builds Metal. + /// A value outside this list stays untouched until the user picks one of these. /// - public static IReadOnlyList OfferedGraphicsApis { get; } = RecompPlatform.IsLinux ? ["vulkan"] : ["d3d12", "vulkan"]; + public static IReadOnlyList OfferedGraphicsApis { get; } = + RecompPlatform.IsLinux ? ["vulkan"] + : RecompPlatform.IsMacOS ? ["metal"] + : ["d3d12", "vulkan"]; /// The label for a graphics API value, for example DirectX 12 for d3d12. public static string DescribeGraphicsApi(string api) => @@ -23,6 +26,7 @@ public static string DescribeGraphicsApi(string api) => { "d3d12" => "DirectX 12", "vulkan" => "Vulkan", + "metal" => "Metal", _ => api, }; diff --git a/WheelWizard/Resources/Languages/en.yml b/WheelWizard/Resources/Languages/en.yml index e60a39d29..33a213569 100644 --- a/WheelWizard/Resources/Languages/en.yml +++ b/WheelWizard/Resources/Languages/en.yml @@ -620,12 +620,12 @@ en: helper_text: topbar: location_warning: "Settings are disabled, complete the location settings first" - enable_recomp: "Uses WiiCompiled as the home-page launcher. Turn this off to use Dolphin and Retro Rewind instead. Windows only." + enable_recomp: "Uses WiiCompiled as the home-page launcher. Turn this off to use Dolphin and Retro Rewind instead. Windows, Linux, and macOS only." recomp_use_dolphin_data: "Use Dolphin's existing Wii folder in place so WiiCompiled shares saves, Miis, and Wii settings. A change takes effect the next time WiiCompiled launches." recomp_share_dolphin_data: "WiiCompiled and Dolphin use the same saves, Miis, and Wii settings." recomp_shared_nand_warning: "Do not run Dolphin and WiiCompiled at the same time while sharing NAND data." recomp_clone_dolphin_data: "Copies Dolphin's current saves, Miis, and Wii settings into private WiiCompiled data." - recomp_graphics_api: "Only DirectX 12 and Vulkan run WiiCompiled reliably, so nothing else is offered." + recomp_graphics_api: "Only the graphics APIs that run WiiCompiled reliably on this system are offered." recomp_dolphin_data_not_found: "Set an existing Dolphin User Folder in Location Settings first. It must contain a Wii folder." recomp_prevent_stutters: "While a shader is still compiling, the objects that need it are skipped for a few frames instead of the whole game stalling until it is ready. You get a tiny bit of pop-in instead of a stutter." launch_with_dolphin: "Will launch dolphins main window along with the game"