Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Flatpak/io.github.TeamWheelWizard.WheelWizard-url-handler.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Desktop Entry]
Version=1.0
Icon=io.github.TeamWheelWizard.WheelWizard
Exec=WheelWizard %U
Terminal=false
NoDisplay=true
StartupNotify=true
Type=Application
Categories=Game;
StartupWMClass=WheelWizard
MimeType=x-scheme-handler/wheelwizard;
Name=Wheel Wizard – URL Handler
GenericName=Mario Kart Wii Mod Manager
Comment=Mario Kart Wii Mod Manager & Retro Rewind Auto Updater
1 change: 1 addition & 0 deletions Flatpak/io.github.TeamWheelWizard.WheelWizard.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</provides>

<releases>
<release version="2.4.11" date="2026-07-09"/>
<release version="2.4.10" date="2026-06-05"/>
<release version="2.4.9" date="2026-06-01"/>
<release version="2.4.8" date="2026-06-01"/>
Expand Down
1 change: 1 addition & 0 deletions WheelWizard.Test/Features/GameBananaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ private GameBananaModPreview CreateFakeModPreview(int id)
},
ModelName = "Mod",
PreviewMedia = new(),
HasContentRatings = false,
};
}

Expand Down
84 changes: 73 additions & 11 deletions WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,29 @@ public LinuxDolphinInstallerTests()
}

[Fact]
public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakInfoExitCodeIsZero()
public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakListHasDolphin()
{
_processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu").Returns(Ok(0));
_processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\norg.DolphinEmu.dolphin-emu\n");

var result = _installer.IsDolphinInstalledInFlatpak();

Assert.True(result);
}

[Fact]
public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakInfoExitCodeIsNonZero()
public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakListHasNoDolphin()
{
_processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu").Returns(Ok(1));
_processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\n");

var result = _installer.IsDolphinInstalledInFlatpak();

Assert.False(result);
}

[Fact]
public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakListFails()
{
_processService.Run("flatpak", "list --app --columns=application", out _, out _).Returns(Ok(1));

var result = _installer.IsDolphinInstalledInFlatpak();

Expand Down Expand Up @@ -77,13 +87,53 @@ public async Task InstallFlatpak_ReturnsSuccess_WhenInstallCompletesAndCommandBe
Assert.True(result.IsSuccess);
}

[Fact]
public async Task InstallFlatpakDolphin_ReturnsFailure_WhenDolphinRemoteCommandFails()
{
_commandEnvironment.IsCommandAvailable("flatpak").Returns(true);
_processService
.Run("flatpak", "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo")
.Returns(Ok(1));
_processService
.Run("flatpak", "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo")
.Returns(Ok(0));

var result = await _installer.InstallFlatpakDolphin();

Assert.True(result.IsFailure);
Assert.Contains("exit code 1", result.Error.Message);
}

[Fact]
public async Task InstallFlatpakDolphin_ReturnsFailure_WhenFlathubRemoteCommandFails()
{
_commandEnvironment.IsCommandAvailable("flatpak").Returns(true);
_processService
.Run("flatpak", "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo")
.Returns(Ok(0));
_processService
.Run("flatpak", "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo")
.Returns(Ok(1));

var result = await _installer.InstallFlatpakDolphin();

Assert.True(result.IsFailure);
Assert.Contains("exit code 1", result.Error.Message);
}

[Fact]
public async Task InstallFlatpakDolphin_ReturnsFailure_WhenDolphinInstallCommandFails()
{
_commandEnvironment.IsCommandAvailable("flatpak").Returns(true);
_processService
.RunWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult<OperationResult<int>>(Ok(1)));
.Run("flatpak", "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo")
.Returns(Ok(0));
_processService
.Run("flatpak", "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo")
.Returns(Ok(0));
_processService
.RunWithProgressAsync("flatpak", "install --user -y dolphin org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult(Ok(1)));

var result = await _installer.InstallFlatpakDolphin();

Expand All @@ -96,8 +146,14 @@ public async Task InstallFlatpakDolphin_ReturnsFailure_WhenWarmupLaunchFails()
{
_commandEnvironment.IsCommandAvailable("flatpak").Returns(true);
_processService
.RunWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult<OperationResult<int>>(Ok(0)));
.Run("flatpak", "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo")
.Returns(Ok(0));
_processService
.Run("flatpak", "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo")
.Returns(Ok(0));
_processService
.RunWithProgressAsync("flatpak", "install --user -y dolphin org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult(Ok(0)));
_processService
.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4))
.Returns(Task.FromResult<OperationResult>(Fail("Launch failed")));
Expand All @@ -113,11 +169,17 @@ public async Task InstallFlatpakDolphin_ReturnsSuccess_WhenInstallAndWarmupSucce
{
_commandEnvironment.IsCommandAvailable("flatpak").Returns(true);
_processService
.RunWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult<OperationResult<int>>(Ok(0)));
.Run("flatpak", "remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo")
.Returns(Ok(0));
_processService
.Run("flatpak", "remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo")
.Returns(Ok(0));
_processService
.RunWithProgressAsync("flatpak", "install --user -y dolphin org.DolphinEmu.dolphin-emu", Arg.Any<IProgress<int>?>())
.Returns(Task.FromResult(Ok(0)));
_processService
.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4))
.Returns(Task.FromResult<OperationResult>(Ok()));
.Returns(Task.FromResult(Ok()));

var result = await _installer.InstallFlatpakDolphin();

Expand Down
23 changes: 10 additions & 13 deletions WheelWizard.Test/Features/Settings/SettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@ public class SettingsManagerTests
[Fact]
public void Get_Throws_WhenRequestedTypeDoesNotMatchSettingType()
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);
var manager = CreateManager(new MockFileSystem(), out _, out _);

Assert.Throws<InvalidOperationException>(() => manager.Get<int>(manager.WW_LANGUAGE));
}

[Fact]
public void Set_Throws_WhenProvidedValueIsNull()
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);
var manager = CreateManager(new MockFileSystem(), out _, out _);

Assert.Throws<ArgumentNullException>(() => manager.Set<string>(manager.WW_LANGUAGE, null!));
}

[Fact]
public void Set_ReturnsFalse_WhenValidationFails()
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);
var manager = CreateManager(new MockFileSystem(), out _, out _);

var result = manager.Set(manager.FOCUSED_USER, 99, skipSave: true);

Expand All @@ -49,7 +49,7 @@ public void Set_ReturnsFalse_WhenValidationFails()
[InlineData(2.01)]
public void SavedWindowScale_RejectsValuesOutsideBounds(double scale)
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);
var manager = CreateManager(new MockFileSystem(), out _, out _);

var result = manager.Set(manager.SAVED_WINDOW_SCALE, scale, skipSave: true);

Expand All @@ -62,7 +62,7 @@ public void SavedWindowScale_RejectsValuesOutsideBounds(double scale)
[InlineData(2.01)]
public void WindowScalePreview_RejectsValuesOutsideBounds(double scale)
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);
var manager = CreateManager(new MockFileSystem(), out _, out _);

var result = manager.Set(manager.WINDOW_SCALE, scale, skipSave: true);

Expand All @@ -73,7 +73,7 @@ public void WindowScalePreview_RejectsValuesOutsideBounds(double scale)
[Fact]
public void ValidateCorePathSettings_ReturnsAllExpectedIssues_WhenDefaultsAreInvalid()
{
var manager = CreateManager(new RealFileSystem(), out _, out _, out _);
var manager = CreateManager(new RealFileSystem(), out _, out _);
#pragma warning disable CS0618
SettingsRuntime.Initialize(manager);
#pragma warning restore CS0618
Expand All @@ -91,7 +91,7 @@ public void ValidateCorePathSettings_ReturnsAllExpectedIssues_WhenDefaultsAreInv
public void PathsSetupCorrectly_ReturnsTrue_WhenCorePathsAreValid()
{
var fileSystem = new MockFileSystem();
var manager = CreateManager(fileSystem, out _, out _, out _);
var manager = CreateManager(fileSystem, out _, out _);
var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}";
var gameFilePath = Path.Combine(userFolderPath, "game.iso");
var dolphinLocation = SettingsTestUtils.GetValidDolphinLocation(fileSystem);
Expand All @@ -110,7 +110,7 @@ public void PathsSetupCorrectly_ReturnsTrue_WhenCorePathsAreValid()
[Fact]
public void LoadSettings_CallsUnderlyingManagersOnlyOnce()
{
var manager = CreateManager(new MockFileSystem(), out var whWzManager, out var dolphinManager, out _);
var manager = CreateManager(new MockFileSystem(), out var whWzManager, out var dolphinManager);

manager.LoadSettings();
manager.LoadSettings();
Expand All @@ -122,16 +122,13 @@ public void LoadSettings_CallsUnderlyingManagersOnlyOnce()
private static SettingsManager CreateManager(
IFileSystem fileSystem,
out IWhWzSettingManager whWzSettingManager,
out IDolphinSettingManager dolphinSettingManager,
out ILinuxDolphinInstaller linuxDolphinInstaller
out IDolphinSettingManager dolphinSettingManager
)
{
whWzSettingManager = Substitute.For<IWhWzSettingManager>();
dolphinSettingManager = Substitute.For<IDolphinSettingManager>();
linuxDolphinInstaller = Substitute.For<ILinuxDolphinInstaller>();
linuxDolphinInstaller.IsDolphinInstalledInFlatpak().Returns(true);

return new SettingsManager(whWzSettingManager, dolphinSettingManager, linuxDolphinInstaller, fileSystem);
return new SettingsManager(whWzSettingManager, dolphinSettingManager, fileSystem);
}
}

Expand Down
44 changes: 38 additions & 6 deletions WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace WheelWizard.DolphinInstaller;
public interface ILinuxDolphinInstaller
{
bool IsDolphinInstalledInFlatpak();
bool IsDolphinInstalledNative();
bool IsFlatpakInstalled();
Task<OperationResult> InstallFlatpak(IProgress<int>? progress = null);
Task<OperationResult> InstallFlatpakDolphin(IProgress<int>? progress = null);
Expand All @@ -13,7 +14,21 @@ public sealed class LinuxDolphinInstaller(ILinuxCommandEnvironment commandEnviro
{
public bool IsDolphinInstalledInFlatpak()
{
var processResult = processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu");
const string dolphinAppId = "org.DolphinEmu.dolphin-emu";
var processResult = processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _);

return processResult.IsSuccess && processResult.Value == 0 && stdOut
.Split('\n')
.Any(line => line == dolphinAppId);
}

public bool IsDolphinInstalledNative()
{
if (!commandEnvironment.IsCommandAvailable("dolphin-emu"))
{
return false;
}
var processResult = processService.Run("dolphin-emu", "--version");
return processResult.IsSuccess && processResult.Value == 0;
}

Expand Down Expand Up @@ -56,17 +71,34 @@ public async Task<OperationResult> InstallFlatpakDolphin(IProgress<int>? progres
return installFlatpakResult;
}

var addRemoteResult = processService.Run(
"flatpak",
"remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo"
);
if (addRemoteResult.IsFailure)
return addRemoteResult.Error;

if (addRemoteResult.Value != 0)
return Fail($"Adding the Dolphin Flatpak remote failed with exit code {addRemoteResult.Value}.");

addRemoteResult = processService.Run(
"flatpak",
"remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo"
);
if (addRemoteResult.IsFailure)
return addRemoteResult.Error;

if (addRemoteResult.Value != 0)
return Fail($"Adding the Flathub Flatpak remote failed with exit code {addRemoteResult.Value}.");

var installDolphinResult = await processService.RunWithProgressAsync(
"pkexec",
"flatpak --system install -y org.DolphinEmu.dolphin-emu",
"flatpak",
"install --user -y dolphin org.DolphinEmu.dolphin-emu",
progress
);
if (installDolphinResult.IsFailure)
return installDolphinResult.Error;

if (installDolphinResult.Value is 126 or 127)
return Fail("You need to be an administrator to install Dolphin via Flatpak.");

if (installDolphinResult.Value != 0)
return Fail($"Dolphin installation failed with exit code {installDolphinResult.Value}.");

Expand Down
18 changes: 16 additions & 2 deletions WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ namespace WheelWizard.DolphinInstaller;

public interface ILinuxProcessService
{
OperationResult<int> Run(string fileName, string arguments, out string stdOut, out string stdErr);
OperationResult<int> Run(string fileName, string arguments);
Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? progress = null);
Task<OperationResult> LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration);
}

public sealed class LinuxProcessService : ILinuxProcessService
{
public OperationResult<int> Run(string fileName, string arguments)
public OperationResult<int> Run(string fileName, string arguments, out string stdOut, out string stdErr)
{
return TryCatch(
var localStdOut = "";
var localStdErr = "";
var result = TryCatch(
() =>
{
var processInfo = new ProcessStartInfo
Expand All @@ -31,11 +34,22 @@ public OperationResult<int> Run(string fileName, string arguments)
if (process == null)
return -1;

localStdOut = process.StandardOutput.ReadToEnd();
localStdErr = process.StandardError.ReadToEnd();
process.WaitForExit();
return process.ExitCode;
},
$"Failed to run process: {fileName} {arguments}"
);

stdOut = localStdOut;
stdErr = localStdErr;
return result;
}

public OperationResult<int> Run(string fileName, string arguments)
{
return Run(fileName, arguments, out _, out _);
}

public async Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? progress = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,8 @@ public class GameBananaModDetails
public required int DownloadCount { get; set; }

[JsonPropertyName("_aFiles")]
public required List<GameBananaModFiles> Files { get; set; }
public List<GameBananaModFiles>? Files { get; set; }

[JsonPropertyName("_aArchivedFiles")]
public List<GameBananaModFiles>? ArchivedFiles { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public class GameBananaModPreview
[JsonPropertyName("_aPreviewMedia")]
public required GameBananaPreviewMedia PreviewMedia { get; set; }

[JsonPropertyName("_bHasContentRatings")]
public required bool HasContentRatings { get; set; }

[JsonPropertyName("_nLikeCount")]
public int LikeCount { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public GameBananaModPreview GetLoadingPreview()
AvatarUrl = "",
},
PreviewMedia = new(),
HasContentRatings = false,
};
}
}
Loading
Loading