diff --git a/.gitignore b/.gitignore index b6d74e93..1c87ff33 100644 --- a/.gitignore +++ b/.gitignore @@ -393,6 +393,9 @@ FodyWeavers.xsd # Local History for Visual Studio Code .history/ +# Avalonia build task artifacts +**/.avalonia-build-tasks/ + # Windows Installer files from build outputs *.cab *.msi diff --git a/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs new file mode 100644 index 00000000..8cef2545 --- /dev/null +++ b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs @@ -0,0 +1,126 @@ +using WheelWizard.DolphinInstaller; +using WheelWizard.Shared; + +namespace WheelWizard.Test.Features; + +public class LinuxDolphinInstallerTests +{ + private readonly ILinuxCommandEnvironment _commandEnvironment; + private readonly ILinuxProcessService _processService; + private readonly LinuxDolphinInstaller _installer; + + public LinuxDolphinInstallerTests() + { + _commandEnvironment = Substitute.For(); + _processService = Substitute.For(); + _installer = new LinuxDolphinInstaller(_commandEnvironment, _processService); + } + + [Fact] + public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakInfoExitCodeIsZero() + { + _processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu").Returns(Ok(0)); + + var result = _installer.IsDolphinInstalledInFlatpak(); + + Assert.True(result); + } + + [Fact] + public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakInfoExitCodeIsNonZero() + { + _processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu").Returns(Ok(1)); + + var result = _installer.IsDolphinInstalledInFlatpak(); + + Assert.False(result); + } + + [Fact] + public async Task InstallFlatpak_ReturnsFailure_WhenPackageManagerCannotBeDetected() + { + _commandEnvironment.IsCommandAvailable("flatpak").Returns(false); + _commandEnvironment.DetectPackageManagerInstallCommand().Returns(string.Empty); + + var result = await _installer.InstallFlatpak(); + + Assert.True(result.IsFailure); + Assert.Contains("Unsupported Linux distribution", result.Error.Message); + } + + [Fact] + public async Task InstallFlatpak_ReturnsFailure_WhenPkexecIsUnauthorized() + { + _commandEnvironment.IsCommandAvailable("flatpak").Returns(false); + _commandEnvironment.DetectPackageManagerInstallCommand().Returns("apt-get install -y"); + _processService + .RunWithProgressAsync("pkexec", "apt-get install -y flatpak", Arg.Any?>()) + .Returns(Task.FromResult>(Ok(126))); + + var result = await _installer.InstallFlatpak(); + + Assert.True(result.IsFailure); + Assert.Contains("administrator", result.Error.Message); + } + + [Fact] + public async Task InstallFlatpak_ReturnsSuccess_WhenInstallCompletesAndCommandBecomesAvailable() + { + _commandEnvironment.IsCommandAvailable("flatpak").Returns(false, true); + _commandEnvironment.DetectPackageManagerInstallCommand().Returns("apt-get install -y"); + _processService + .RunWithProgressAsync("pkexec", "apt-get install -y flatpak", Arg.Any?>()) + .Returns(Task.FromResult>(Ok(0))); + + var result = await _installer.InstallFlatpak(); + + Assert.True(result.IsSuccess); + } + + [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?>()) + .Returns(Task.FromResult>(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_WhenWarmupLaunchFails() + { + _commandEnvironment.IsCommandAvailable("flatpak").Returns(true); + _processService + .RunWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", Arg.Any?>()) + .Returns(Task.FromResult>(Ok(0))); + _processService + .LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4)) + .Returns(Task.FromResult(Fail("Launch failed"))); + + var result = await _installer.InstallFlatpakDolphin(); + + Assert.True(result.IsFailure); + Assert.Equal("Launch failed", result.Error.Message); + } + + [Fact] + public async Task InstallFlatpakDolphin_ReturnsSuccess_WhenInstallAndWarmupSucceed() + { + _commandEnvironment.IsCommandAvailable("flatpak").Returns(true); + _processService + .RunWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", Arg.Any?>()) + .Returns(Task.FromResult>(Ok(0))); + _processService + .LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4)) + .Returns(Task.FromResult(Ok())); + + var result = await _installer.InstallFlatpakDolphin(); + + Assert.True(result.IsSuccess); + } +} diff --git a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs new file mode 100644 index 00000000..d1647f84 --- /dev/null +++ b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs @@ -0,0 +1,149 @@ +using Testably.Abstractions.Testing; +using WheelWizard.Services; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Test.Features.Settings; + +[Collection("SettingsFeature")] +public class DolphinSettingTests +{ + [Fact] + public void Constructor_Throws_WhenFileNameIsNotIni() + { + var action = () => new DolphinSetting(typeof(string), ("Dolphin.cfg", "General", "NANDRootPath"), "value"); + + Assert.Throws(action); + } + + [Fact] + public void SetFromString_ParsesEnumAndFormatsAsIntegerString() + { + var setting = new DolphinSetting( + typeof(DolphinShaderCompilationMode), + ("GFX.ini", "Settings", "ShaderCompilationMode"), + DolphinShaderCompilationMode.Default + ); + + var result = setting.SetFromString("2", skipSave: true); + + Assert.True(result); + Assert.Equal(DolphinShaderCompilationMode.HybridUberShaders, Assert.IsType(setting.Get())); + Assert.Equal("2", setting.GetStringValue()); + } + + [Fact] + public void Set_ReturnsFalseAndKeepsOldValue_WhenValidationFails() + { + var setting = new DolphinSetting(typeof(int), ("GFX.ini", "Settings", "InternalResolution"), 1).SetValidation(value => + (int)value! >= 0 + ); + setting.Set(2); + + var result = setting.Set(-1); + + Assert.False(result); + Assert.Equal(2, Assert.IsType(setting.Get())); + } + + [Fact] + public void SetFromString_Throws_WhenTypeIsUnsupported() + { + var setting = new DolphinSetting(typeof(decimal), ("GFX.ini", "Settings", "Price"), 1m); + + Assert.Throws(() => setting.SetFromString("3.14")); + } +} + +[Collection("SettingsFeature")] +public class DolphinSettingManagerTests : IDisposable +{ + [Fact] + public void LoadSettings_ReadsExistingValue_FromIniFile() + { + var fileSystem = new MockFileSystem(); + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + SettingsTestUtils.InitializeSettingsRuntime(userFolderPath); + var configFolderPath = PathManager.ConfigFolderPath; + var iniPath = Path.Combine(configFolderPath, "Dolphin.ini"); + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllLines(iniPath, ["[General]", "NANDRootPath = /persisted"]); + var manager = new DolphinSettingManager(fileSystem); + var setting = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "/default"); + + manager.RegisterSetting(setting); + manager.LoadSettings(); + + Assert.Equal("/persisted", Assert.IsType(setting.Get())); + } + + [Fact] + public void LoadSettings_WritesDefaultValue_WhenIniEntryIsMissing() + { + var fileSystem = new MockFileSystem(); + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + SettingsTestUtils.InitializeSettingsRuntime(userFolderPath); + var configFolderPath = PathManager.ConfigFolderPath; + var iniPath = Path.Combine(configFolderPath, "Dolphin.ini"); + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllLines(iniPath, ["[General]", "OtherSetting = 1"]); + var manager = new DolphinSettingManager(fileSystem); + var setting = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "/default"); + + manager.RegisterSetting(setting); + manager.LoadSettings(); + + var updatedFile = fileSystem.File.ReadAllText(iniPath); + Assert.Contains("NANDRootPath = /default", updatedFile); + } + + [Fact] + public void SaveSettings_UpdatesExistingSettingLine_InIniFile() + { + var fileSystem = new MockFileSystem(); + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + SettingsTestUtils.InitializeSettingsRuntime(userFolderPath); + var configFolderPath = PathManager.ConfigFolderPath; + var iniPath = Path.Combine(configFolderPath, "Dolphin.ini"); + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllLines(iniPath, ["[General]", "NANDRootPath = /old"]); + var manager = new DolphinSettingManager(fileSystem); + var setting = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "/default"); + + manager.RegisterSetting(setting); + manager.LoadSettings(); + setting.Set("/new", skipSave: true); + manager.SaveSettings(setting); + + var updatedFile = fileSystem.File.ReadAllText(iniPath); + Assert.Contains("NANDRootPath = /new", updatedFile); + Assert.DoesNotContain("NANDRootPath = /old", updatedFile); + } + + [Fact] + public void ReloadSettings_ReReadsFile_AfterItChangesOnDisk() + { + var fileSystem = new MockFileSystem(); + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + SettingsTestUtils.InitializeSettingsRuntime(userFolderPath); + var configFolderPath = PathManager.ConfigFolderPath; + var iniPath = Path.Combine(configFolderPath, "Dolphin.ini"); + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllLines(iniPath, ["[General]", "NANDRootPath = /first"]); + var manager = new DolphinSettingManager(fileSystem); + var setting = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "/default"); + + manager.RegisterSetting(setting); + manager.LoadSettings(); + fileSystem.File.WriteAllLines(iniPath, ["[General]", "NANDRootPath = /second"]); + manager.ReloadSettings(); + + Assert.Equal("/second", Assert.IsType(setting.Get())); + } + + public void Dispose() + { + SettingsTestUtils.ResetSettingsRuntime(); + SettingsTestUtils.ResetSignalRuntime(); + } +} diff --git a/WheelWizard.Test/Features/Settings/SettingsTests.cs b/WheelWizard.Test/Features/Settings/SettingsTests.cs new file mode 100644 index 00000000..3b974b37 --- /dev/null +++ b/WheelWizard.Test/Features/Settings/SettingsTests.cs @@ -0,0 +1,334 @@ +using System.Globalization; +using System.IO.Abstractions; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Testably.Abstractions; +using Testably.Abstractions.Testing; +using WheelWizard.DolphinInstaller; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Test.Features.Settings; + +[CollectionDefinition("SettingsFeature", DisableParallelization = true)] +public sealed class SettingsFeatureCollection; + +[Collection("SettingsFeature")] +public class SettingsManagerTests +{ + [Fact] + public void Get_Throws_WhenRequestedTypeDoesNotMatchSettingType() + { + var manager = CreateManager(new MockFileSystem(), out _, out _, out _); + + Assert.Throws(() => manager.Get(manager.WW_LANGUAGE)); + } + + [Fact] + public void Set_Throws_WhenProvidedValueIsNull() + { + var manager = CreateManager(new MockFileSystem(), out _, out _, out _); + + Assert.Throws(() => manager.Set(manager.WW_LANGUAGE, null!)); + } + + [Fact] + public void Set_ReturnsFalse_WhenValidationFails() + { + var manager = CreateManager(new MockFileSystem(), out _, out _, out _); + + var result = manager.Set(manager.FOCUSED_USER, 99, skipSave: true); + + Assert.False(result); + Assert.Equal(0, manager.Get(manager.FOCUSED_USER)); + } + + [Fact] + public void ValidateCorePathSettings_ReturnsAllExpectedIssues_WhenDefaultsAreInvalid() + { + var manager = CreateManager(new RealFileSystem(), out _, out _, out _); +#pragma warning disable CS0618 + SettingsRuntime.Initialize(manager); +#pragma warning restore CS0618 + + var result = manager.ValidateCorePathSettings(); + + Assert.True(result.IsSuccess); + Assert.False(result.Value.IsValid); + Assert.Contains(result.Value.Issues, issue => issue.Code == SettingsValidationCode.InvalidUserFolderPath); + Assert.Contains(result.Value.Issues, issue => issue.Code == SettingsValidationCode.InvalidDolphinLocation); + Assert.Contains(result.Value.Issues, issue => issue.Code == SettingsValidationCode.InvalidGameLocation); + } + + [Fact] + public void PathsSetupCorrectly_ReturnsTrue_WhenCorePathsAreValid() + { + var fileSystem = new MockFileSystem(); + var manager = CreateManager(fileSystem, out _, out _, out _); + var userFolderPath = $"/wheelwizard-user-{Guid.NewGuid():N}"; + var gameFilePath = Path.Combine(userFolderPath, "game.iso"); + var dolphinLocation = SettingsTestUtils.GetValidDolphinLocation(fileSystem); + fileSystem.Directory.CreateDirectory(userFolderPath); + fileSystem.File.WriteAllText(gameFilePath, "iso"); +#pragma warning disable CS0618 + SettingsRuntime.Initialize(manager); +#pragma warning restore CS0618 + + Assert.True(manager.Set(manager.USER_FOLDER_PATH, userFolderPath, skipSave: true)); + Assert.True(manager.Set(manager.GAME_LOCATION, gameFilePath, skipSave: true)); + Assert.True(manager.Set(manager.DOLPHIN_LOCATION, dolphinLocation, skipSave: true)); + Assert.True(manager.PathsSetupCorrectly()); + } + + [Fact] + public void LoadSettings_CallsUnderlyingManagersOnlyOnce() + { + var manager = CreateManager(new MockFileSystem(), out var whWzManager, out var dolphinManager, out _); + + manager.LoadSettings(); + manager.LoadSettings(); + + whWzManager.Received(1).LoadSettings(); + dolphinManager.Received(1).LoadSettings(); + } + + private static SettingsManager CreateManager( + IFileSystem fileSystem, + out IWhWzSettingManager whWzSettingManager, + out IDolphinSettingManager dolphinSettingManager, + out ILinuxDolphinInstaller linuxDolphinInstaller + ) + { + whWzSettingManager = Substitute.For(); + dolphinSettingManager = Substitute.For(); + linuxDolphinInstaller = Substitute.For(); + linuxDolphinInstaller.IsDolphinInstalledInFlatpak().Returns(true); + + return new SettingsManager(whWzSettingManager, dolphinSettingManager, linuxDolphinInstaller, fileSystem); + } +} + +[Collection("SettingsFeature")] +public class SettingsSignalBusTests +{ + [Fact] + public void Publish_NotifiesActiveSubscribers() + { + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var setting = new WhWzSetting(typeof(int), "Volume", 10); + SettingChangedSignal? receivedSignal = null; + using var _ = signalBus.Subscribe(signal => receivedSignal = signal); + + signalBus.Publish(setting); + + Assert.True(receivedSignal.HasValue); + Assert.Same(setting, receivedSignal.Value.Setting); + } + + [Fact] + public void DisposeSubscription_StopsReceivingSignals() + { + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var setting = new WhWzSetting(typeof(int), "Volume", 10); + var receiveCount = 0; + var subscription = signalBus.Subscribe(_ => receiveCount++); + + signalBus.Publish(setting); + subscription.Dispose(); + signalBus.Publish(setting); + + Assert.Equal(1, receiveCount); + } + + [Fact] + public void Subscribe_Throws_WhenHandlerIsNull() + { + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + + Assert.Throws(() => signalBus.Subscribe(null!)); + } +} + +[Collection("SettingsFeature")] +public class SettingsLocalizationServiceTests +{ + [Fact] + public void Initialize_SetsCurrentCulture_FromLanguageSetting() + { + var originalCulture = CultureInfo.CurrentCulture; + var originalUiCulture = CultureInfo.CurrentUICulture; + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var settingsManager = Substitute.For(); + var languageSetting = new WhWzSetting(typeof(string), "WW_Language", "fr"); + settingsManager.WW_LANGUAGE.Returns(languageSetting); + settingsManager.Get(Arg.Any()).Returns(_ => (string)languageSetting.Get()); + var localizationService = new SettingsLocalizationService(settingsManager, signalBus); + + try + { + localizationService.Initialize(); + + Assert.Equal("fr", CultureInfo.CurrentCulture.TwoLetterISOLanguageName); + Assert.Equal("fr", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.CurrentUICulture = originalUiCulture; + } + } + + [Fact] + public void PublishLanguageSignal_UpdatesCulture_WhenLanguageChanges() + { + var originalCulture = CultureInfo.CurrentCulture; + var originalUiCulture = CultureInfo.CurrentUICulture; + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var settingsManager = Substitute.For(); + var languageSetting = new WhWzSetting(typeof(string), "WW_Language", "en"); + settingsManager.WW_LANGUAGE.Returns(languageSetting); + settingsManager.Get(Arg.Any()).Returns(_ => (string)languageSetting.Get()); + var localizationService = new SettingsLocalizationService(settingsManager, signalBus); + + try + { + localizationService.Initialize(); + languageSetting.Set("de", skipSave: true); + signalBus.Publish(languageSetting); + + Assert.Equal("de", CultureInfo.CurrentCulture.TwoLetterISOLanguageName); + Assert.Equal("de", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.CurrentUICulture = originalUiCulture; + } + } +} + +[Collection("SettingsFeature")] +public class SettingsStartupInitializerTests +{ + [Fact] + public void Initialize_LoadsSettings_InitializesLocalization_AndSetsRuntimes() + { + var settingsManager = Substitute.For(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var localizationService = Substitute.For(); + var logger = Substitute.For>(); + settingsManager.ValidateCorePathSettings().Returns(Ok(new SettingsValidationReport([]))); + var initializer = new SettingsStartupInitializer(settingsManager, signalBus, localizationService, logger); + + initializer.Initialize(); + + settingsManager.Received(1).LoadSettings(); + localizationService.Received(1).Initialize(); +#pragma warning disable CS0618 + Assert.Same(settingsManager, SettingsRuntime.Current); +#pragma warning restore CS0618 + } + + [Fact] + public void Initialize_DoesNotThrow_WhenValidationFails() + { + var settingsManager = Substitute.For(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); + var localizationService = Substitute.For(); + var logger = Substitute.For>(); + settingsManager.ValidateCorePathSettings().Returns(Fail("validation failed")); + var initializer = new SettingsStartupInitializer(settingsManager, signalBus, localizationService, logger); + + var exception = Record.Exception(initializer.Initialize); + + Assert.Null(exception); + settingsManager.Received(1).LoadSettings(); + localizationService.Received(1).Initialize(); + } +} + +internal static class SettingsTestUtils +{ + public static ISettingsManager InitializeSettingsRuntime(string userFolderPath, string dolphinLocation = "dolphin-emu") + { + var settings = CreateRuntimeSettingsStub(userFolderPath, dolphinLocation); +#pragma warning disable CS0618 + SettingsRuntime.Initialize(settings); +#pragma warning restore CS0618 + return settings; + } + + public static void InitializeSignalRuntime(ISettingsSignalBus? signalBus = null) + { +#pragma warning disable CS0618 + SettingsSignalRuntime.Initialize(signalBus ?? CreateSettingsSignalBus()); +#pragma warning restore CS0618 + } + + public static ISettingsSignalBus CreateSettingsSignalBus() + { + var logger = Substitute.For>(); + return new SettingsSignalBus(logger); + } + + public static void ResetSettingsRuntime() + { +#pragma warning disable CS0618 + SetPrivateStaticFieldValue(typeof(SettingsRuntime), "_current", null); +#pragma warning restore CS0618 + } + + public static void ResetSignalRuntime() + { +#pragma warning disable CS0618 + SetPrivateStaticFieldValue(typeof(SettingsSignalRuntime), "_current", null); + var pendingInitializersField = + typeof(SettingsSignalRuntime).GetField("PendingInitializers", BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("SettingsSignalRuntime pending initializers field was not found."); +#pragma warning restore CS0618 + if (pendingInitializersField.GetValue(null) is not System.Collections.IList pendingInitializers) + throw new InvalidOperationException("SettingsSignalRuntime pending initializers storage has an unexpected type."); + pendingInitializers.Clear(); + } + + public static string GetValidDolphinLocation(IFileSystem fileSystem) + { + if (!OperatingSystem.IsWindows()) + return "/usr/bin/env"; + + const string exePath = @"C:\WheelWizardTests\Dolphin.exe"; + var directoryPath = fileSystem.Path.GetDirectoryName(exePath); + if (!string.IsNullOrWhiteSpace(directoryPath)) + fileSystem.Directory.CreateDirectory(directoryPath); + + fileSystem.File.WriteAllText(exePath, "test"); + return exePath; + } + + private static ISettingsManager CreateRuntimeSettingsStub(string userFolderPath, string dolphinLocation) + { + var settings = Substitute.For(); + var userFolderSetting = new WhWzSetting(typeof(string), "UserFolderPath", userFolderPath); + var dolphinLocationSetting = new WhWzSetting(typeof(string), "DolphinLocation", dolphinLocation); + + settings.USER_FOLDER_PATH.Returns(userFolderSetting); + settings.DOLPHIN_LOCATION.Returns(dolphinLocationSetting); + + settings + .Get(Arg.Is(setting => ReferenceEquals(setting, userFolderSetting))) + .Returns(_ => (string)userFolderSetting.Get()); + settings + .Get(Arg.Is(setting => ReferenceEquals(setting, dolphinLocationSetting))) + .Returns(_ => (string)dolphinLocationSetting.Get()); + + return settings; + } + + private static void SetPrivateStaticFieldValue(Type targetType, string fieldName, object? value) + { + var field = + targetType.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException($"{targetType.Name}.{fieldName} field was not found."); + field.SetValue(null, value); + } +} diff --git a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs new file mode 100644 index 00000000..c9c77c8c --- /dev/null +++ b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs @@ -0,0 +1,57 @@ +using WheelWizard.Settings; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Test.Features.Settings; + +[Collection("SettingsFeature")] +public class VirtualSettingTests +{ + [Fact] + public void Set_StoresValueAndInvokesSetter_WhenValueIsValid() + { + var backingValue = 1; + var setting = new VirtualSetting(typeof(int), value => backingValue = (int)value, () => backingValue); + + var result = setting.Set(5); + + Assert.True(result); + Assert.Equal(5, backingValue); + Assert.Equal(5, Assert.IsType(setting.Get())); + } + + [Fact] + public void Set_ReturnsFalseAndKeepsOldValue_WhenValidationFails() + { + var backingValue = 2; + var setting = new VirtualSetting(typeof(int), value => backingValue = (int)value, () => backingValue).SetValidation(value => + (int)value! >= 0 + ); + + var result = setting.Set(-1); + + Assert.False(result); + Assert.Equal(2, backingValue); + Assert.Equal(2, Assert.IsType(setting.Get())); + } + + [Fact] + public void SetDependencies_RecalculatesValue_WhenDependencySignalsChange() + { + SettingsTestUtils.InitializeSignalRuntime(SettingsTestUtils.CreateSettingsSignalBus()); + var dependency = new WhWzSetting(typeof(int), "Dependency", 1); + var setting = new VirtualSetting(typeof(int), _ => { }, () => (int)dependency.Get()).SetDependencies(dependency); + + dependency.Set(7, skipSave: true); + + Assert.Equal(7, Assert.IsType(setting.Get())); + } + + [Fact] + public void SetDependencies_Throws_WhenCalledTwice() + { + var dependency = new WhWzSetting(typeof(int), "Dependency", 1); + var setting = new VirtualSetting(typeof(int), _ => { }, () => 1).SetDependencies(dependency); + + Assert.Throws(() => setting.SetDependencies(dependency)); + } +} diff --git a/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs b/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs new file mode 100644 index 00000000..9a82b942 --- /dev/null +++ b/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs @@ -0,0 +1,159 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Testably.Abstractions.Testing; +using WheelWizard.Services; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Test.Features.Settings; + +[Collection("SettingsFeature")] +public class WhWzSettingTests +{ + [Fact] + public void Set_StoresValueAndCallsSaveAction_WhenValueIsValid() + { + var saveCalls = 0; + var setting = new WhWzSetting(typeof(int), "Volume", 10, _ => saveCalls++); + + var result = setting.Set(20); + + Assert.True(result); + Assert.Equal(20, Assert.IsType(setting.Get())); + Assert.Equal(1, saveCalls); + } + + [Fact] + public void Set_ReturnsFalseAndKeepsOldValue_WhenValidationFails() + { + var setting = new WhWzSetting(typeof(int), "Volume", 10).SetValidation(value => (int)value! >= 0); + setting.Set(5); + + var result = setting.Set(-1); + + Assert.False(result); + Assert.Equal(5, Assert.IsType(setting.Get())); + } + + [Fact] + public void Reset_AppliesDefaultValue_EvenIfDefaultDoesNotPassValidation() + { + var saveCalls = 0; + var setting = new WhWzSetting(typeof(int), "Threshold", 5, _ => saveCalls++).SetValidation(value => (int)value! >= 10); + setting.Set(12); + + setting.Reset(); + + Assert.Equal(5, Assert.IsType(setting.Get())); + Assert.Equal(2, saveCalls); + } + + [Fact] + public void SetFromJson_ParsesEnumAndArrayValues() + { + var enumSetting = new WhWzSetting(typeof(DayOfWeek), "Day", DayOfWeek.Monday); + var arraySetting = new WhWzSetting(typeof(string[]), "Names", Array.Empty()); + using var enumDocument = JsonDocument.Parse("2"); + using var arrayDocument = JsonDocument.Parse("[\"A\", \"B\"]"); + + var enumResult = enumSetting.SetFromJson(enumDocument.RootElement, skipSave: true); + var arrayResult = arraySetting.SetFromJson(arrayDocument.RootElement, skipSave: true); + + Assert.True(enumResult); + Assert.True(arrayResult); + Assert.Equal(DayOfWeek.Tuesday, Assert.IsType(enumSetting.Get())); + Assert.Equal(["A", "B"], Assert.IsType(arraySetting.Get())); + } + + [Fact] + public void SetFromJson_Throws_WhenTypeIsUnsupported() + { + var setting = new WhWzSetting(typeof(decimal), "Price", 1m); + using var document = JsonDocument.Parse("2"); + + Assert.Throws(() => setting.SetFromJson(document.RootElement, skipSave: true)); + } +} + +[Collection("SettingsFeature")] +public class WhWzSettingManagerTests +{ + [Fact] + public void LoadSettings_AppliesPersistedValues_ToRegisteredSettings() + { + var fileSystem = new MockFileSystem(); + var logger = Substitute.For>(); + var manager = new WhWzSettingManager(logger, fileSystem); + var volume = new WhWzSetting(typeof(int), "Volume", 5).SetValidation(value => (int)value! >= 0); + var language = new WhWzSetting(typeof(string), "Language", "en"); + var configPath = PathManager.WheelWizardConfigFilePath; + var configFolderPath = fileSystem.Path.GetDirectoryName(configPath)!; + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllText(configPath, "{\"Volume\":12,\"Language\":\"de\",\"Unknown\":true}"); + + manager.RegisterSetting(volume); + manager.RegisterSetting(language); + manager.LoadSettings(); + + Assert.Equal(12, Assert.IsType(volume.Get())); + Assert.Equal("de", Assert.IsType(language.Get())); + } + + [Fact] + public void LoadSettings_ResetsInvalidPersistedValues_ToDefaults() + { + var fileSystem = new MockFileSystem(); + var logger = Substitute.For>(); + var manager = new WhWzSettingManager(logger, fileSystem); + var volume = new WhWzSetting(typeof(int), "Volume", 5).SetValidation(value => (int)value! >= 0); + var configPath = PathManager.WheelWizardConfigFilePath; + var configFolderPath = fileSystem.Path.GetDirectoryName(configPath)!; + fileSystem.Directory.CreateDirectory(configFolderPath); + fileSystem.File.WriteAllText(configPath, "{\"Volume\":-1}"); + + manager.RegisterSetting(volume); + manager.LoadSettings(); + + Assert.Equal(5, Assert.IsType(volume.Get())); + } + + [Fact] + public void SaveSettings_PersistsRegisteredValues_AfterLoad() + { + var fileSystem = new MockFileSystem(); + var logger = Substitute.For>(); + var manager = new WhWzSettingManager(logger, fileSystem); + var volume = new WhWzSetting(typeof(int), "Volume", 5); + var configPath = PathManager.WheelWizardConfigFilePath; + + manager.RegisterSetting(volume); + manager.LoadSettings(); + volume.Set(9, skipSave: true); + manager.SaveSettings(volume); + + var savedJson = fileSystem.File.ReadAllText(configPath); + Assert.Contains("\"Volume\": 9", savedJson); + } + + [Fact] + public void RegisterSetting_IsIgnoredAfterLoad() + { + var fileSystem = new MockFileSystem(); + var logger = Substitute.For>(); + var manager = new WhWzSettingManager(logger, fileSystem); + var registeredBeforeLoad = new WhWzSetting(typeof(int), "Volume", 1); + var ignoredAfterLoad = new WhWzSetting(typeof(string), "Future", "initial"); + var configPath = PathManager.WheelWizardConfigFilePath; + + manager.RegisterSetting(registeredBeforeLoad); + manager.LoadSettings(); + manager.RegisterSetting(ignoredAfterLoad); + registeredBeforeLoad.Set(2, skipSave: true); + ignoredAfterLoad.Set("changed", skipSave: true); + manager.SaveSettings(registeredBeforeLoad); + + var savedJson = fileSystem.File.ReadAllText(configPath); + Assert.Contains("\"Volume\": 2", savedJson); + Assert.DoesNotContain("Future", savedJson); + } +} diff --git a/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs b/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs index a65a8acf..304c66bb 100644 --- a/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs +++ b/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs @@ -1,6 +1,7 @@ using System.IO.Abstractions; using Microsoft.Extensions.Logging; using WheelWizard.CustomDistributions.Domain; +using WheelWizard.Settings; using WheelWizard.Shared.Services; namespace WheelWizard.CustomDistributions; @@ -21,10 +22,15 @@ public class CustomDistributionSingletonService : ICustomDistributionSingletonSe public RetroRewind RetroRewind { get; } public RetroRewindBeta RetroRewindBeta { get; } - public CustomDistributionSingletonService(IFileSystem fileSystem, IApiCaller api, ILogger logger) + public CustomDistributionSingletonService( + IFileSystem fileSystem, + IApiCaller api, + ILogger logger, + ISettingsManager settingsManager + ) { - RetroRewind = new RetroRewind(fileSystem, api, logger); - RetroRewindBeta = new RetroRewindBeta(fileSystem, logger); + RetroRewind = new RetroRewind(fileSystem, api, logger, settingsManager); + RetroRewindBeta = new RetroRewindBeta(fileSystem, logger, settingsManager); } public List GetAllDistributions() diff --git a/WheelWizard/Features/CustomDistributions/RetroRewind.cs b/WheelWizard/Features/CustomDistributions/RetroRewind.cs index f47c39b2..c63e34ca 100644 --- a/WheelWizard/Features/CustomDistributions/RetroRewind.cs +++ b/WheelWizard/Features/CustomDistributions/RetroRewind.cs @@ -9,7 +9,7 @@ using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.Services; using WheelWizard.Views.Popups.Generic; @@ -20,12 +20,19 @@ public class RetroRewind : IDistribution private readonly IFileSystem _fileSystem; private readonly IApiCaller _api; private readonly ILogger _logger; + private readonly ISettingsManager _settingsManager; - public RetroRewind(IFileSystem fileSystem, IApiCaller api, ILogger logger) + public RetroRewind( + IFileSystem fileSystem, + IApiCaller api, + ILogger logger, + ISettingsManager settingsManager + ) { _api = api; _fileSystem = fileSystem; _logger = logger; + _settingsManager = settingsManager; } public string Title => "Retro Rewind"; @@ -566,7 +573,7 @@ public async Task ReinstallAsync(ProgressWindow progressWindow) public async Task> GetCurrentStatusAsync() { - if (!SettingsHelper.PathsSetupCorrectly()) + if (!_settingsManager.PathsSetupCorrectly()) return WheelWizardStatus.ConfigNotFinished; var serverEnabled = await _api.CallApiAsync(api => api.Ping()); diff --git a/WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs b/WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs index 75166dd8..de759416 100644 --- a/WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs +++ b/WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs @@ -10,7 +10,7 @@ using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.CustomDistributions; @@ -19,11 +19,13 @@ public class RetroRewindBeta : IDistribution { private readonly IFileSystem _fileSystem; private readonly ILogger _logger; + private readonly ISettingsManager _settingsManager; - public RetroRewindBeta(IFileSystem fileSystem, ILogger logger) + public RetroRewindBeta(IFileSystem fileSystem, ILogger logger, ISettingsManager settingsManager) { _fileSystem = fileSystem; _logger = logger; + _settingsManager = settingsManager; } public string Title => "Retro Rewind Beta"; @@ -162,7 +164,7 @@ public async Task ReinstallAsync(ProgressWindow progressWindow) public Task> GetCurrentStatusAsync() { - if (!SettingsHelper.PathsSetupCorrectly()) + if (!_settingsManager.PathsSetupCorrectly()) return Task.FromResult(Ok(WheelWizardStatus.ConfigNotFinished)); var isInstalled = diff --git a/WheelWizard/Features/DolphinInstaller/DolphinInstallerExtensions.cs b/WheelWizard/Features/DolphinInstaller/DolphinInstallerExtensions.cs new file mode 100644 index 00000000..56fa6cde --- /dev/null +++ b/WheelWizard/Features/DolphinInstaller/DolphinInstallerExtensions.cs @@ -0,0 +1,18 @@ +namespace WheelWizard.DolphinInstaller; + +public static class DolphinInstallerExtensions +{ + public static IServiceCollection AddDolphinInstaller(this IServiceCollection services) + { + // TODO: Reorganize this feature boundary. + // Right now this registers 3 service concerns: + // 1) Linux command environment, 2) Linux process execution, 3) Dolphin installer orchestration. + // Consider either: + // - moving Linux command/process services into a shared Linux feature/module, or + // - using a strategy-based installer (like AutoUpdater) with platform/version-specific installer implementations. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} diff --git a/WheelWizard/Features/DolphinInstaller/LinuxCommandEnvironment.cs b/WheelWizard/Features/DolphinInstaller/LinuxCommandEnvironment.cs new file mode 100644 index 00000000..8803026a --- /dev/null +++ b/WheelWizard/Features/DolphinInstaller/LinuxCommandEnvironment.cs @@ -0,0 +1,22 @@ +using WheelWizard.Helpers; + +namespace WheelWizard.DolphinInstaller; + +public interface ILinuxCommandEnvironment +{ + bool IsCommandAvailable(string command); + string DetectPackageManagerInstallCommand(); +} + +public sealed class LinuxCommandEnvironment : ILinuxCommandEnvironment +{ + public bool IsCommandAvailable(string command) + { + return EnvHelper.IsValidUnixCommand(command); + } + + public string DetectPackageManagerInstallCommand() + { + return EnvHelper.DetectLinuxPackageManagerInstallCommand(); + } +} diff --git a/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs new file mode 100644 index 00000000..af2f5725 --- /dev/null +++ b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs @@ -0,0 +1,76 @@ +namespace WheelWizard.DolphinInstaller; + +public interface ILinuxDolphinInstaller +{ + bool IsDolphinInstalledInFlatpak(); + bool IsFlatpakInstalled(); + Task InstallFlatpak(IProgress? progress = null); + Task InstallFlatpakDolphin(IProgress? progress = null); +} + +public sealed class LinuxDolphinInstaller(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService) + : ILinuxDolphinInstaller +{ + public bool IsDolphinInstalledInFlatpak() + { + var processResult = processService.Run("flatpak", "info org.DolphinEmu.dolphin-emu"); + return processResult.IsSuccess && processResult.Value == 0; + } + + public bool IsFlatpakInstalled() + { + return commandEnvironment.IsCommandAvailable("flatpak"); + } + + public async Task InstallFlatpak(IProgress? progress = null) + { + if (IsFlatpakInstalled()) + return Ok(); + + var packageManagerCommand = commandEnvironment.DetectPackageManagerInstallCommand(); + if (string.IsNullOrWhiteSpace(packageManagerCommand)) + return Fail("Unsupported Linux distribution. Could not detect a package manager command."); + + var installResult = await processService.RunWithProgressAsync("pkexec", $"{packageManagerCommand} flatpak", progress); + if (installResult.IsFailure) + return installResult.Error; + + if (installResult.Value is 126 or 127) + return Fail("You need to be an administrator to install Flatpak."); + + if (installResult.Value != 0) + return Fail($"Flatpak installation failed with exit code {installResult.Value}."); + + if (!IsFlatpakInstalled()) + return Fail("Flatpak installation completed, but Flatpak is still unavailable."); + + return Ok(); + } + + public async Task InstallFlatpakDolphin(IProgress? progress = null) + { + if (!IsFlatpakInstalled()) + { + var installFlatpakResult = await InstallFlatpak(progress); + if (installFlatpakResult.IsFailure) + return installFlatpakResult; + } + + var installDolphinResult = await processService.RunWithProgressAsync( + "pkexec", + "flatpak --system install -y 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}."); + + var launchResult = await processService.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4)); + return launchResult.IsFailure ? launchResult.Error : Ok(); + } +} diff --git a/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs b/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs new file mode 100644 index 00000000..f1c53112 --- /dev/null +++ b/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace WheelWizard.DolphinInstaller; + +public interface ILinuxProcessService +{ + OperationResult Run(string fileName, string arguments); + Task> RunWithProgressAsync(string fileName, string arguments, IProgress? progress = null); + Task LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration); +} + +public sealed class LinuxProcessService : ILinuxProcessService +{ + public OperationResult Run(string fileName, string arguments) + { + return TryCatch( + () => + { + var processInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(processInfo); + if (process == null) + return -1; + + process.WaitForExit(); + return process.ExitCode; + }, + $"Failed to run process: {fileName} {arguments}" + ); + } + + public async Task> RunWithProgressAsync(string fileName, string arguments, IProgress? progress = null) + { + return await TryCatch( + async () => + { + var processInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(processInfo); + if (process == null) + return -1; + + process.OutputDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress); + process.ErrorDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync(); + return process.ExitCode; + }, + $"Failed to run process: {fileName} {arguments}" + ); + } + + public async Task LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration) + { + return await TryCatch( + async () => + { + using var process = new Process + { + StartInfo = new() + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + + process.Start(); + await Task.Delay(duration); + + if (!process.HasExited) + process.Kill(); + }, + $"Failed to run process: {fileName} {arguments}" + ); + } + + private static void ReportProgress(string? output, IProgress? progress) + { + if (string.IsNullOrWhiteSpace(output)) + return; + + var match = Regex.Match(output, @"(\d+)%"); + if (match.Success && int.TryParse(match.Groups[1].Value, out var percent)) + progress?.Report(percent); + } +} diff --git a/WheelWizard/Features/Settings/ADDING_SETTINGS.md b/WheelWizard/Features/Settings/ADDING_SETTINGS.md new file mode 100644 index 00000000..8f296e55 --- /dev/null +++ b/WheelWizard/Features/Settings/ADDING_SETTINGS.md @@ -0,0 +1,57 @@ +# Adding a Setting in WheelWizard + +## Setting Types +- **WheelWizard:** Our own settings, we save them in a JSON file. +- **Dolphin:** Settings from the Dolphin emulator. they store them in INI files. this implementation allows us to also modify them= +- **Virtual:** Settings that are not saved. These are used for managing for computing state and managing side effect. For instance, if you want to control 3 settings with 1 toggle, virtual settings is perfect for that. + +## Adding settings +You first always define the setting in the `ISettingsServices.cs` file in the `ISettingsProperties` class +```csharp +Setting MY_NEW_SETTING { get; } +``` +then you also define this setting in the `SettingsManager.cs` as a property +```csharp +public Setting MY_NEW_SETTING { get; } +``` + +after that you have to register the setting. This depends on the type of setting you want to add. + +### Wheel Wizard +```csharp +MY_NEW_SETTING = RegisterWhWz( + "MyNewSetting", + false, + value => value is bool +); +``` + +### Dolphin +```csharp +MY_DOLPHIN_SETTING = RegisterDolphin( + ("GFX.ini", "Settings", "MyDolphinKey"), + 0, + value => (int)(value ?? -1) >= 0 +); +``` + +### Virtual +```csharp +MY_VIRTUAL_SETTING = new VirtualSetting( + typeof(bool), + value => { /* apply side-effects */ }, + () => { /* compute value */ return true; } +).SetDependencies(SETTING_A, SETTING_B); +``` +Usually, you create virtual settings that reference one or more real settings. +The value of the virtual setting is cached. However, if the value relies on, for example, SETTING_A, then once SETTING_A changes, your cache is incorrect. +For that reason, you have to set dependencies. That way, if SETTING_A changes, the virtual setting gets a signal to recompute its value. + +## Reading/Writing settings +Use type-safe manager methods in callers: +```csharp +// reading +bool value = SettingsManager.Get(SettingsManager.MY_NEW_SETTING); +// writeing +SettingsManager.Set(SettingsManager.MY_NEW_SETTING, true); +``` diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs new file mode 100644 index 00000000..0a1f7090 --- /dev/null +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -0,0 +1,180 @@ +using System.IO.Abstractions; +using WheelWizard.Services; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +public class DolphinSettingManager(IFileSystem fileSystem) : IDolphinSettingManager +{ + private string ConfigFolderPath(string fileName) => fileSystem.Path.Combine(PathManager.ConfigFolderPath, fileName); + + // LOCKS: + // We use locks to keep the settings state and file IO consistent. + // Even though we do not manually create threads in this class, work can still happen concurrently + // (for example the Avalonia UI thread + Task/thread-pool execution), so synchronization is still required. + + // Sync Root: Responsible for synchronizing access to the _settings list and the _loaded flag. + // It ensures that multiple threads don't modify the settings list or the loaded state at the same time + // File IO Sync: Responsible for reading and writing the INI files. It ensures that multiple threads don't read/write at the same time + private readonly object _syncRoot = new(); + private readonly object _fileIoSync = new(); + private bool _loaded; + private readonly List _settings = []; + + public void RegisterSetting(DolphinSetting setting) + { + lock (_syncRoot) + { + if (_loaded) + return; + + _settings.Add(setting); + } + } + + public void SaveSettings(DolphinSetting invokingSetting) + { + List settingsSnapshot; + lock (_syncRoot) + { + // TODO: This method definitely has to be optimized + if (!_loaded) + return; + + settingsSnapshot = [.. _settings]; + } + + lock (_fileIoSync) + { + foreach (var setting in settingsSnapshot) + { + ChangeIniSettings(setting.FileName, setting.Section, setting.Name, setting.GetStringValue()); + } + } + } + + public void ReloadSettings() + { + lock (_syncRoot) + { + // TODO: this method could also be optimized by checking if the previously loaded directory + // is still the current ConfigFolderPath and if so, just not run the LoadSettings method again + _loaded = false; + } + + LoadSettings(); + } + + public void LoadSettings() + { + List settingsSnapshot; + if (_loaded || !fileSystem.Directory.Exists(PathManager.ConfigFolderPath)) + return; + + lock (_syncRoot) + { + // Since we are working with concurrency here, we have to check loaded again since it might be changed while we were waiting + // for the lock to open + if (_loaded) + return; + + _loaded = true; + settingsSnapshot = [.. _settings]; + } + + // TODO: This method can maybe be optimized in the future, since now it reads the file for every setting + // and on top of that for reach setting it loops over each line and section and stuff like that. + lock (_fileIoSync) + { + foreach (var setting in settingsSnapshot) + { + var value = ReadIniSetting(setting.FileName, setting.Section, setting.Name); + if (value == null) + ChangeIniSettings(setting.FileName, setting.Section, setting.Name, setting.GetStringValue()); + else + setting.SetFromString(value, true); // we read it, which means there is no purpose in saving it again + } + } + } + + private string[]? ReadIniFile(string fileName) + { + var filePath = ConfigFolderPath(fileName); + if (!fileSystem.File.Exists(filePath)) + return null; + + try + { + return fileSystem.File.ReadAllLines(filePath); + } + catch + { + return null; + } + } + + private string? ReadIniSetting(string fileName, string section, string settingToRead) + { + var lines = ReadIniFile(fileName); + if (lines == null) + return null; + + var sectionIndex = Array.IndexOf(lines, $"[{section}]"); + if (sectionIndex == -1) + return null; + + // find all the settings related to this section, we dont want to read/influence other sections + var nextSectionName = lines.Skip(sectionIndex + 1).FirstOrDefault(x => x.Trim().StartsWith("[") && x.Trim().EndsWith("]")); + var nextSectionIndex = Array.IndexOf(lines, nextSectionName); + var sectionLines = lines.Skip(sectionIndex + 1); + if (nextSectionIndex != -1) + sectionLines = sectionLines.Take(nextSectionIndex - sectionIndex - 1); + + // finally we can read the setting + foreach (var line in sectionLines) + { + if (!line.StartsWith($"{settingToRead}=") && !line.StartsWith($"{settingToRead} =")) + continue; + //we found the setting, now we need to return the value + var setting = line.Split("="); + return setting[1].Trim(); + } + + return null; + } + + // TODO: find out when to use `setting=value` and when to use `setting = value` + private void ChangeIniSettings(string fileName, string section, string settingToChange, string value) + { + var lines = ReadIniFile(fileName)?.ToList(); + if (lines == null) + return; + + var sectionIndex = lines.IndexOf($"[{section}]"); + if (sectionIndex == -1) + { + lines.Add($"[{section}]"); + lines.Add($"{settingToChange} = {value}"); + fileSystem.File.WriteAllLines(ConfigFolderPath(fileName), lines); + return; + } + + for (var i = sectionIndex + 1; i < lines.Count; i++) + { + // + if (lines[i].Trim().StartsWith("[") && lines[i].Trim().EndsWith("]")) + break; // Setting was not found in this section, so we have to append it to the section + + if (!lines[i].StartsWith($"{settingToChange}=") && !lines[i].StartsWith($"{settingToChange} =")) + continue; + + lines[i] = $"{settingToChange} = {value}"; + fileSystem.File.WriteAllLines(ConfigFolderPath(fileName), lines); + return; + } + // you only get here if the setting was not found in the section + + lines.Insert(sectionIndex + 1, $"{settingToChange} = {value}"); + fileSystem.File.WriteAllLines(ConfigFolderPath(fileName), lines); + } +} diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs new file mode 100644 index 00000000..063518a3 --- /dev/null +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -0,0 +1,64 @@ +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +public interface IWhWzSettingManager +{ + void RegisterSetting(WhWzSetting setting); + void SaveSettings(WhWzSetting invokingSetting); + void LoadSettings(); +} + +public interface IDolphinSettingManager +{ + void RegisterSetting(DolphinSetting setting); + void SaveSettings(DolphinSetting invokingSetting); + void ReloadSettings(); + void LoadSettings(); +} + +public interface ISettingsProperties +{ + Setting USER_FOLDER_PATH { get; } + Setting DOLPHIN_LOCATION { get; } + Setting GAME_LOCATION { get; } + Setting FORCE_WIIMOTE { get; } + Setting LAUNCH_WITH_DOLPHIN { get; } + Setting PREFERS_MODS_ROW_VIEW { get; } + Setting FOCUSED_USER { get; } + Setting ENABLE_ANIMATIONS { get; } + Setting TESTING_MODE_ENABLED { get; } + Setting SAVED_WINDOW_SCALE { get; } + Setting REMOVE_BLUR { get; } + Setting RR_REGION { get; } + Setting WW_LANGUAGE { get; } + Setting NAND_ROOT_PATH { get; } + Setting LOAD_PATH { get; } + Setting VSYNC { get; } + Setting INTERNAL_RESOLUTION { get; } + Setting SHOW_FPS { get; } + Setting GFX_BACKEND { get; } + Setting MACADDRESS { get; } + Setting WINDOW_SCALE { get; } + Setting RECOMMENDED_SETTINGS { get; } +} + +public interface ISettingsManager : ISettingsProperties +{ + OperationResult ValidateCorePathSettings(); + + T Get(Setting setting); + bool Set(Setting setting, T value, bool skipSave = false); + bool PathsSetupCorrectly(); + void LoadSettings(); +} + +public interface ISettingsStartupInitializer +{ + void Initialize(); +} + +public interface ISettingsLocalizationService +{ + void Initialize(); +} diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs new file mode 100644 index 00000000..eb8e8a0b --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -0,0 +1,23 @@ +namespace WheelWizard.Settings; + +public static class SettingsExtensions +{ + public static IServiceCollection AddSettings(this IServiceCollection services) + { + // TODO(naming-cleanup): + // - Prefix casing is inconsistent: `WhWz*` vs `WW_*` (example: `WhWzSettingManager`, `WW_LANGUAGE`). + // - Some setting identifiers use all-caps while others use PascalCase (example: `MACADDRESS` vs `GAME_LOCATION`). + // - Domain type naming is mixed between generic and feature-specific terms (`Setting`, `WhWzSetting`, `DolphinSetting`). + + // TODO: Investigate / migrate to IOptions: https://learn.microsoft.com/en-us/dotnet/core/extensions/options + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/WheelWizard/Features/Settings/SettingsLocalizationService.cs b/WheelWizard/Features/Settings/SettingsLocalizationService.cs new file mode 100644 index 00000000..5017e00a --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsLocalizationService.cs @@ -0,0 +1,33 @@ +using System.Globalization; + +namespace WheelWizard.Settings; + +public sealed class SettingsLocalizationService(ISettingsManager settingsManager, ISettingsSignalBus settingsSignalBus) + : ISettingsLocalizationService +{ + private bool _initialized; + private IDisposable? _subscription; + + public void Initialize() + { + if (_initialized) + return; + + _subscription = settingsSignalBus.Subscribe(OnSignal); + ApplyCulture(); + _initialized = true; + } + + private void OnSignal(SettingChangedSignal signal) + { + if (signal.Setting == settingsManager.WW_LANGUAGE) + ApplyCulture(); + } + + private void ApplyCulture() + { + var newCulture = new CultureInfo(settingsManager.Get(settingsManager.WW_LANGUAGE)); + CultureInfo.CurrentCulture = newCulture; + CultureInfo.CurrentUICulture = newCulture; + } +} diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs new file mode 100644 index 00000000..d9ae7adf --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -0,0 +1,299 @@ +using System.IO.Abstractions; +using System.Runtime.InteropServices; +using WheelWizard.DolphinInstaller; +using WheelWizard.Helpers; +using WheelWizard.Models.Enums; +using WheelWizard.Services; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +public class SettingsManager : ISettingsManager +{ + private readonly IWhWzSettingManager _whWzSettingManager; + private readonly IDolphinSettingManager _dolphinSettingManager; + private readonly ILinuxDolphinInstaller _linuxDolphinInstaller; + private readonly IFileSystem _fileSystem; + + private readonly Setting _dolphinCompilationMode; + private readonly Setting _dolphinCompileShadersAtStart; + private readonly Setting _dolphinSsaa; + private readonly Setting _dolphinMsaa; + + private bool _hasLoadedSettings; + private double _internalScale = -1.0; + + #region Constructor + public SettingsManager( + IWhWzSettingManager whWzSettingManager, + IDolphinSettingManager dolphinSettingManager, + ILinuxDolphinInstaller linuxDolphinInstaller, + IFileSystem fileSystem + ) + { + _whWzSettingManager = whWzSettingManager; + _dolphinSettingManager = dolphinSettingManager; + _linuxDolphinInstaller = linuxDolphinInstaller; + _fileSystem = fileSystem; + + #region WhWz settings + DOLPHIN_LOCATION = RegisterWhWz( + "DolphinLocation", + "", + value => + { + var pathOrCommand = value as string ?? string.Empty; + if (string.IsNullOrWhiteSpace(pathOrCommand)) + return false; + + if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return EnvHelper.IsValidUnixCommand(pathOrCommand); + + if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak()) + return false; + + return EnvHelper.IsValidUnixCommand(pathOrCommand); + } + + return _fileSystem.File.Exists(pathOrCommand); + } + ); + + USER_FOLDER_PATH = RegisterWhWz( + "UserFolderPath", + "", + value => + { + var userFolderPath = value as string ?? string.Empty; + if (!_fileSystem.Directory.Exists(userFolderPath)) + return false; + + var dolphinLocation = Get(DOLPHIN_LOCATION); + + // We cannot determine the validity of the user folder path in that case + if (string.IsNullOrWhiteSpace(dolphinLocation)) + return true; + + // If we want to use a split XDG dolphin config, + // this only really works as expected if certain conditions are met. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || !PathManager.IsLinuxDolphinConfigSplit()) + return true; + + // In this case, Dolphin would use `EMBEDDED_USER_DIR` (portable `user` directory). + if (_fileSystem.Directory.Exists("user")) + return false; + + // The Dolphin executable directory with `portable.txt` case + if (_fileSystem.File.Exists(Path.Combine(PathManager.GetDolphinExeDirectory(), "portable.txt"))) + return false; + + // The value of this environment variable would be used instead if it was somehow set + const string environmentVariableToAvoid = "DOLPHIN_EMU_USERPATH"; + + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(environmentVariableToAvoid))) + return false; + + if (dolphinLocation.Contains(environmentVariableToAvoid, StringComparison.Ordinal)) + return false; + + // `~/.dolphin-emu` would be used if it exists + if (!PathManager.IsFlatpakDolphinFilePath() && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath)) + return false; + + return true; + } + ); + + GAME_LOCATION = RegisterWhWz("GameLocation", "", value => _fileSystem.File.Exists(value as string ?? string.Empty)); + FORCE_WIIMOTE = RegisterWhWz("ForceWiimote", false); + LAUNCH_WITH_DOLPHIN = RegisterWhWz("LaunchWithDolphin", false); + PREFERS_MODS_ROW_VIEW = RegisterWhWz("PrefersModsRowView", true); + FOCUSED_USER = RegisterWhWz("FavoriteUser", 0, value => (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4); + + ENABLE_ANIMATIONS = RegisterWhWz("EnableAnimations", true); + TESTING_MODE_ENABLED = RegisterWhWz("TestingModeEnabled", false); + SAVED_WINDOW_SCALE = RegisterWhWz("WindowScale", 1.0, value => (double)(value ?? -1) >= 0.5 && (double)(value ?? -1) <= 2.0); + REMOVE_BLUR = RegisterWhWz("REMOVE_BLUR", true); + RR_REGION = RegisterWhWz("RR_Region", MarioKartWiiEnums.Regions.None); + WW_LANGUAGE = RegisterWhWz("WW_Language", "en", value => SettingValues.WhWzLanguages.ContainsKey((string)value!)); + #endregion + + #region Dolphin settings + NAND_ROOT_PATH = RegisterDolphin( + ("Dolphin.ini", "General", "NANDRootPath"), + "", + value => _fileSystem.Directory.Exists(value as string ?? string.Empty) + ); + + LOAD_PATH = RegisterDolphin( + ("Dolphin.ini", "General", "LoadPath"), + "", + value => _fileSystem.Directory.Exists(value as string ?? string.Empty) + ); + + VSYNC = RegisterDolphin(("GFX.ini", "Hardware", "VSync"), false); + INTERNAL_RESOLUTION = RegisterDolphin(("GFX.ini", "Settings", "InternalResolution"), 1, value => (int)(value ?? -1) >= 0); + SHOW_FPS = RegisterDolphin(("GFX.ini", "Settings", "ShowFPS"), false); + GFX_BACKEND = RegisterDolphin(("Dolphin.ini", "Core", "GFXBackend"), SettingValues.GFXRenderers.Values.First()); + + // recommended settings + _dolphinCompilationMode = RegisterDolphin(("GFX.ini", "Settings", "ShaderCompilationMode"), DolphinShaderCompilationMode.Default); + _dolphinCompileShadersAtStart = RegisterDolphin(("GFX.ini", "Settings", "WaitForShadersBeforeStarting"), false); + _dolphinSsaa = RegisterDolphin(("GFX.ini", "Settings", "SSAA"), false); + _dolphinMsaa = RegisterDolphin( + ("GFX.ini", "Settings", "MSAA"), + "0x00000001", + value => (value?.ToString() ?? "") is "0x00000001" or "0x00000002" or "0x00000004" or "0x00000008" + ); + + // Readonly settings + MACADDRESS = RegisterDolphin(("Dolphin.ini", "General", "WirelessMac"), "02:01:02:03:04:05"); + #endregion + + #region Virtual settings + WINDOW_SCALE = new VirtualSetting( + typeof(double), + value => _internalScale = (double)value!, + () => _internalScale == -1.0 ? SAVED_WINDOW_SCALE.Get() : _internalScale + ).SetDependencies(SAVED_WINDOW_SCALE); + + RECOMMENDED_SETTINGS = new VirtualSetting( + typeof(bool), + value => + { + var newValue = (bool)value!; + _dolphinCompilationMode.Set( + newValue ? DolphinShaderCompilationMode.HybridUberShaders : DolphinShaderCompilationMode.Default + ); +#if WINDOWS + _dolphinCompileShadersAtStart.Set(newValue); +#endif + _dolphinMsaa.Set(newValue ? "0x00000002" : "0x00000001"); + _dolphinSsaa.Set(false); + }, + () => + { + var value1 = (DolphinShaderCompilationMode)_dolphinCompilationMode.Get(); + var value2 = true; +#if WINDOWS + value2 = (bool)_dolphinCompileShadersAtStart.Get(); +#endif + var value3 = (string)_dolphinMsaa.Get(); + var value4 = (bool)_dolphinSsaa.Get(); + return !value4 && value2 && value3 == "0x00000002" && value1 == DolphinShaderCompilationMode.HybridUberShaders; + } + ).SetDependencies(_dolphinCompilationMode, _dolphinCompileShadersAtStart, _dolphinMsaa, _dolphinSsaa); + #endregion + } + #endregion + + #region Settings Properties + public Setting USER_FOLDER_PATH { get; } + public Setting DOLPHIN_LOCATION { get; } + public Setting GAME_LOCATION { get; } + public Setting FORCE_WIIMOTE { get; } + public Setting LAUNCH_WITH_DOLPHIN { get; } + public Setting PREFERS_MODS_ROW_VIEW { get; } + public Setting FOCUSED_USER { get; } + public Setting ENABLE_ANIMATIONS { get; } + public Setting TESTING_MODE_ENABLED { get; } + public Setting SAVED_WINDOW_SCALE { get; } + public Setting REMOVE_BLUR { get; } + public Setting RR_REGION { get; } + public Setting WW_LANGUAGE { get; } + + public Setting NAND_ROOT_PATH { get; } + public Setting LOAD_PATH { get; } + public Setting VSYNC { get; } + public Setting INTERNAL_RESOLUTION { get; } + public Setting SHOW_FPS { get; } + public Setting GFX_BACKEND { get; } + public Setting MACADDRESS { get; } + public Setting WINDOW_SCALE { get; } + public Setting RECOMMENDED_SETTINGS { get; } + #endregion + + #region Public API + public T Get(Setting setting) + { + var value = setting.Get(); + if (value is not T typedValue) + throw new InvalidOperationException($"Setting '{setting.Name}' does not match expected type '{typeof(T).Name}'."); + + return typedValue; + } + + public bool Set(Setting setting, T value, bool skipSave = false) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return setting.Set(value, skipSave); + } + + public bool PathsSetupCorrectly() + { + var reportResult = ValidateCorePathSettings(); + return reportResult.IsSuccess && reportResult.Value.IsValid; + } + + public OperationResult ValidateCorePathSettings() + { + try + { + var issues = new List(); + + if (!USER_FOLDER_PATH.IsValid()) + issues.Add(new(SettingsValidationCode.InvalidUserFolderPath, USER_FOLDER_PATH.Name, "User folder path is invalid.")); + + if (!DOLPHIN_LOCATION.IsValid()) + issues.Add( + new(SettingsValidationCode.InvalidDolphinLocation, DOLPHIN_LOCATION.Name, "Dolphin path or command is invalid.") + ); + + if (!GAME_LOCATION.IsValid()) + issues.Add(new(SettingsValidationCode.InvalidGameLocation, GAME_LOCATION.Name, "Game file path is invalid.")); + + return Ok(new SettingsValidationReport(issues)); + } + catch (Exception ex) + { + return Fail(ex); + } + } + + public void LoadSettings() + { + if (_hasLoadedSettings) + return; + + _whWzSettingManager.LoadSettings(); + _dolphinSettingManager.LoadSettings(); + _hasLoadedSettings = true; + } + #endregion + + #region Registration Helpers + private WhWzSetting RegisterWhWz(string name, T defaultValue, Func? validation = null) + { + var setting = new WhWzSetting(typeof(T), name, defaultValue!, _whWzSettingManager.SaveSettings); + if (validation != null) + setting.SetValidation(validation); + + _whWzSettingManager.RegisterSetting(setting); + return setting; + } + + private DolphinSetting RegisterDolphin((string, string, string) location, T defaultValue, Func? validation = null) + { + var setting = new DolphinSetting(typeof(T), location, defaultValue!, _dolphinSettingManager.SaveSettings); + if (validation != null) + setting.SetValidation(validation); + + _dolphinSettingManager.RegisterSetting(setting); + return setting; + } + #endregion +} diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs new file mode 100644 index 00000000..2692494c --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -0,0 +1,64 @@ +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +// Legacy runtime bridge for static callers that cannot use constructor injection yet. +// Replace usage with injected services: +// 1) Inject `ISettingsManager` into classes that currently read `SettingsRuntime.Current`. +// 2) Inject `ISettingsSignalBus` for signal subscription/publish usage. +// 3) Remove runtime initialization from `SettingsStartupInitializer` after all static callers are gone. +[Obsolete("SettingsRuntime is deprecated. Use constructor injection for ISettingsManager instead.")] +public static class SettingsRuntime +{ + private static ISettingsManager? _current; + + public static ISettingsManager Current + { + get { return _current ?? throw new InvalidOperationException("Settings runtime has not been initialized yet."); } + } + + public static void Initialize(ISettingsManager settingsManager) + { + _current = settingsManager; + } +} + +[Obsolete("SettingsSignalRuntime is deprecated. Use constructor injection for ISettingsSignalBus instead.")] +public static class SettingsSignalRuntime +{ + private static ISettingsSignalBus? _current; + private static readonly List> PendingInitializers = []; + + public static void Initialize(ISettingsSignalBus signalBus) + { + ArgumentNullException.ThrowIfNull(signalBus); + + _current = signalBus; + var callbacksToRun = PendingInitializers.ToArray(); + PendingInitializers.Clear(); + + foreach (var callback in callbacksToRun) + { + callback(signalBus); + } + } + + public static void OnInitialized(Action callback) + { + ArgumentNullException.ThrowIfNull(callback); + + var signalBus = _current; + if (signalBus == null) + { + PendingInitializers.Add(callback); + return; + } + + callback(signalBus); + } + + public static void Publish(Setting setting) + { + _current?.Publish(setting); + } +} diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs new file mode 100644 index 00000000..460001ec --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Logging; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +public readonly record struct SettingChangedSignal(Setting Setting); + +public interface ISettingsSignalBus +{ + IDisposable Subscribe(Action handler); + void Publish(Setting setting); +} + +public sealed class SettingsSignalBus(ILogger logger) : ISettingsSignalBus +{ + // LOCKS: + // We are working with locks. This is to ensure that we always have accurate information in our settings / application. + // We do not create multiple threads. However, some of our features run through Tasks. Those are executed asynchronously, therefore still require locks. + + private readonly object _syncSubscribers = new(); + private readonly Dictionary> _subscribers = []; + private long _nextSubscriberId; + + public IDisposable Subscribe(Action handler) + { + ArgumentNullException.ThrowIfNull(handler); + + long id; + lock (_syncSubscribers) + { + id = _nextSubscriberId++; + _subscribers[id] = handler; + } + + return new Subscription(this, id); + } + + public void Publish(Setting setting) + { + Action[] handlers; + + // You could use a lock for reading the subscribes. But let's minimize the lock usage to where it is important. + // If the handlers list is slightly outdated it is not a problem (unlike when this happens when modifying this list) + handlers = [.. _subscribers.Values]; + + var signal = new SettingChangedSignal(setting); + foreach (var handler in handlers) + { + try + { + handler(signal); + } + catch + { + // Exceptions from subscribers should not affect the publisher or other subscribers, so we catch and log them. + logger.LogError("A subscriber threw an exception while handling a setting changed signal."); + } + } + } + + private void Unsubscribe(long subscriberId) + { + lock (_syncSubscribers) + { + _subscribers.Remove(subscriberId); + } + } + + private sealed class Subscription(SettingsSignalBus bus, long subscriberId) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + bus.Unsubscribe(subscriberId); + _disposed = true; + } + } +} diff --git a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs new file mode 100644 index 00000000..b07757aa --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; + +namespace WheelWizard.Settings; + +public sealed class SettingsStartupInitializer( + ISettingsManager settingsManager, + ISettingsSignalBus settingsSignalBus, + ISettingsLocalizationService localizationService, + ILogger logger +) : ISettingsStartupInitializer +{ + public void Initialize() + { + SettingsSignalRuntime.Initialize(settingsSignalBus); + SettingsRuntime.Initialize(settingsManager); + settingsManager.LoadSettings(); + localizationService.Initialize(); + + var reportResult = settingsManager.ValidateCorePathSettings(); + if (reportResult.IsFailure) + { + logger.LogError(reportResult.Error.Exception, "Failed to validate startup settings: {Message}", reportResult.Error.Message); + return; + } + + var report = reportResult.Value; + if (report.IsValid) + return; + + foreach (var issue in report.Issues) + { + logger.LogWarning( + "Settings validation warning: {Code} ({SettingName}) {Message}", + issue.Code, + issue.SettingName, + issue.Message + ); + } + } +} diff --git a/WheelWizard/Features/Settings/SettingsValidationReport.cs b/WheelWizard/Features/Settings/SettingsValidationReport.cs new file mode 100644 index 00000000..30496068 --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsValidationReport.cs @@ -0,0 +1,31 @@ +namespace WheelWizard.Settings; + +public enum SettingsValidationCode +{ + InvalidUserFolderPath, + InvalidDolphinLocation, + InvalidGameLocation, +} + +public sealed class SettingsValidationIssue(SettingsValidationCode code, string settingName, string message) +{ + public SettingsValidationCode Code { get; } = code; + public string SettingName { get; } = settingName; + public string Message { get; } = message; + + public override string ToString() => $"[{Code}] {SettingName}: {Message}"; +} + +public sealed class SettingsValidationReport(IReadOnlyList issues) +{ + public IReadOnlyList Issues { get; } = issues; + public bool IsValid => Issues.Count == 0; + + public string ToSummaryText() + { + if (IsValid) + return "All required settings are valid."; + + return string.Join("; ", Issues.Select(issue => issue.ToString())); + } +} diff --git a/WheelWizard/Models/Settings/DolphinSetting.cs b/WheelWizard/Features/Settings/Types/DolphinSetting.cs similarity index 76% rename from WheelWizard/Models/Settings/DolphinSetting.cs rename to WheelWizard/Features/Settings/Types/DolphinSetting.cs index 84a5b0ad..4bc67365 100644 --- a/WheelWizard/Models/Settings/DolphinSetting.cs +++ b/WheelWizard/Features/Settings/Types/DolphinSetting.cs @@ -1,15 +1,19 @@ -using WheelWizard.Services.Settings; - -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Types; public class DolphinSetting : Setting { + private readonly Action _saveAction; + public string FileName { get; private set; } public string Section { get; private set; } public DolphinSetting(Type type, (string, string, string) location, object defaultValue) + : this(type, location, defaultValue, _ => { }) { } + + public DolphinSetting(Type type, (string, string, string) location, object defaultValue, Action saveAction) : base(type, location.Item3, defaultValue) { + _saveAction = saveAction ?? throw new ArgumentNullException(nameof(saveAction)); FileName = location.Item1; Section = location.Item2; // name/key = location.Item3 @@ -19,8 +23,6 @@ public DolphinSetting(Type type, (string, string, string) location, object defau throw new ArgumentException( $"FileName for dolphin setting '[{Section}]{Name}' must end with .ini (given file is '{FileName}')" ); - - DolphinSettingManager.Instance.RegisterSetting(this); } protected override bool SetInternal(object newValue, bool skipSave = false) @@ -31,7 +33,7 @@ protected override bool SetInternal(object newValue, bool skipSave = false) if (newIsValid) { if (!skipSave) - DolphinSettingManager.Instance.SaveSettings(this); + _saveAction(this); } else Value = oldValue; @@ -43,6 +45,18 @@ protected override bool SetInternal(object newValue, bool skipSave = false) public override bool IsValid() => ValidationFunc == null || ValidationFunc(Value); + public new DolphinSetting SetValidation(Func validationFunc) + { + base.SetValidation(validationFunc); + return this; + } + + public new DolphinSetting SetForceSave(bool saveEvenIfNotValid) + { + base.SetForceSave(saveEvenIfNotValid); + return this; + } + public string GetStringValue() { if (ValueType.IsEnum) diff --git a/WheelWizard/Models/Settings/Setting.cs b/WheelWizard/Features/Settings/Types/Setting.cs similarity index 72% rename from WheelWizard/Models/Settings/Setting.cs rename to WheelWizard/Features/Settings/Types/Setting.cs index 183bf060..09c58f88 100644 --- a/WheelWizard/Models/Settings/Setting.cs +++ b/WheelWizard/Features/Settings/Types/Setting.cs @@ -1,4 +1,6 @@ -namespace WheelWizard.Models.Settings; +using WheelWizard.Settings; + +namespace WheelWizard.Settings.Types; public abstract class Setting { @@ -10,7 +12,6 @@ protected Setting(Type type, string name, object defaultValue) ValueType = type; } - protected readonly List DependentVirtualSettings = []; public string Name { get; protected set; } public object DefaultValue { get; protected set; } protected object Value { get; set; } @@ -59,19 +60,5 @@ public Setting SetForceSave(bool saveEvenIfNotValid) return this; } - public bool Unsubscribe(ISettingListener dependent) => DependentVirtualSettings.Remove(dependent); - - public void Subscribe(ISettingListener dependent) - { - if (!DependentVirtualSettings.Contains(dependent)) - DependentVirtualSettings.Add(dependent); - } - - protected void SignalChange() - { - foreach (var dependent in DependentVirtualSettings) - { - dependent.OnSettingChanged(this); - } - } + protected void SignalChange() => SettingsSignalRuntime.Publish(this); } diff --git a/WheelWizard/Models/Settings/SettingConstants.cs b/WheelWizard/Features/Settings/Types/SettingConstants.cs similarity index 97% rename from WheelWizard/Models/Settings/SettingConstants.cs rename to WheelWizard/Features/Settings/Types/SettingConstants.cs index bd1de544..73508520 100644 --- a/WheelWizard/Models/Settings/SettingConstants.cs +++ b/WheelWizard/Features/Settings/Types/SettingConstants.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Types; public enum DolphinShaderCompilationMode { diff --git a/WheelWizard/Models/Settings/VirtualSetting.cs b/WheelWizard/Features/Settings/Types/VirtualSetting.cs similarity index 71% rename from WheelWizard/Models/Settings/VirtualSetting.cs rename to WheelWizard/Features/Settings/Types/VirtualSetting.cs index 03b20ec5..770ab8ae 100644 --- a/WheelWizard/Models/Settings/VirtualSetting.cs +++ b/WheelWizard/Features/Settings/Types/VirtualSetting.cs @@ -1,18 +1,21 @@ -namespace WheelWizard.Models.Settings; +using WheelWizard.Settings; -public class VirtualSetting : Setting, ISettingListener +namespace WheelWizard.Settings.Types; + +public class VirtualSetting : Setting { private Setting[] _dependencies; - private Action Setter; - private Func Getter; + private readonly Action _setter; + private readonly Func _getter; private bool _acceptsSignals = true; + private IDisposable? _signalSubscription; public VirtualSetting(Type type, Action setter, Func getter) : base(type, "virtual", getter()) { _dependencies = []; - Setter = setter; - Getter = getter; + _setter = setter; + _getter = getter; } protected override bool SetInternal(object newValue, bool skipSave = false) @@ -25,7 +28,7 @@ protected override bool SetInternal(object newValue, bool skipSave = false) var succeeded = false; if (newIsValid) { - Setter(newValue); + _setter(newValue); succeeded = true; } else @@ -48,25 +51,29 @@ public VirtualSetting SetDependencies(params Setting[] dependencies) throw new ArgumentException("Dependencies have already been set once"); _dependencies = dependencies; - foreach (var dependency in dependencies) + SettingsSignalRuntime.OnInitialized(signalBus => { - dependency.Subscribe(this); - } + _signalSubscription?.Dispose(); + _signalSubscription = signalBus.Subscribe(OnSignal); + }); return this; } public void Recalculate() { - Value = Getter(); + Value = _getter(); } - public void OnSettingChanged(Setting changedSetting) + private void OnSignal(SettingChangedSignal signal) { if (!_acceptsSignals) return; - SignalChange(); + if (!_dependencies.Contains(signal.Setting)) + return; + Recalculate(); + SignalChange(); } } diff --git a/WheelWizard/Models/Settings/WhWzSetting.cs b/WheelWizard/Features/Settings/Types/WhWzSetting.cs similarity index 75% rename from WheelWizard/Models/Settings/WhWzSetting.cs rename to WheelWizard/Features/Settings/Types/WhWzSetting.cs index 7a76c884..43a4c582 100644 --- a/WheelWizard/Models/Settings/WhWzSetting.cs +++ b/WheelWizard/Features/Settings/Types/WhWzSetting.cs @@ -1,14 +1,18 @@ using System.Text.Json; -using WheelWizard.Services.Settings; -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Types; public class WhWzSetting : Setting { + private readonly Action _saveAction; + public WhWzSetting(Type type, string name, object defaultValue) + : this(type, name, defaultValue, _ => { }) { } + + public WhWzSetting(Type type, string name, object defaultValue, Action saveAction) : base(type, name, defaultValue) { - WhWzSettingManager.Instance.RegisterSetting(this); + _saveAction = saveAction ?? throw new ArgumentNullException(nameof(saveAction)); } protected override bool SetInternal(object newValue, bool skipSave = false) @@ -19,7 +23,7 @@ protected override bool SetInternal(object newValue, bool skipSave = false) if (newIsValid) { if (!skipSave) - WhWzSettingManager.Instance.SaveSettings(this); + _saveAction(this); } else Value = oldValue; @@ -31,6 +35,18 @@ protected override bool SetInternal(object newValue, bool skipSave = false) public override bool IsValid() => ValidationFunc == null || ValidationFunc(Value); + public new WhWzSetting SetValidation(Func validationFunc) + { + base.SetValidation(validationFunc); + return this; + } + + public new WhWzSetting SetForceSave(bool saveEvenIfNotValid) + { + base.SetForceSave(saveEvenIfNotValid); + return this; + } + public bool SetFromJson(JsonElement newValue, bool skipSave = false) { // Feel free to add more types if you find them diff --git a/WheelWizard/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs new file mode 100644 index 00000000..4a7accdc --- /dev/null +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -0,0 +1,131 @@ +using System.IO.Abstractions; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using WheelWizard.Services; +using WheelWizard.Settings.Types; + +namespace WheelWizard.Settings; + +public class WhWzSettingManager(ILogger logger, IFileSystem fileSystem) : IWhWzSettingManager +{ + // LOCKS: + // We are working with locks. This is to ensure that we always have accurate information in our settings / application. + // We do not create multiple threads. However, some of our features run through Tasks. Those are executed asynchronously, therefore still require locks. + + // Sync Root: Responsible for synchronizing access to the _settings list and the _loaded flag. + // It ensures that multiple threads don't modify the settings list or the loaded state at the same time + // File IO Sync: Responsible for reading and writing the INI files. It ensures that multiple threads don't read/write at the same time + private readonly object _syncRoot = new(); + private readonly object _fileIoSync = new(); + private bool _loaded; + private readonly Dictionary _settings = new(); + + public void RegisterSetting(WhWzSetting setting) + { + lock (_syncRoot) + { + if (_loaded) + return; + + _settings[setting.Name] = setting; + } + } + + public void SaveSettings(WhWzSetting invokingSetting) + { + Dictionary settingsSnapshot; + lock (_syncRoot) + { + if (!_loaded) + return; + + settingsSnapshot = new(_settings); + } + + var settingsToSave = new Dictionary(); + + foreach (var (name, setting) in settingsSnapshot) + { + settingsToSave[name] = setting.Get(); + } + + var jsonString = JsonSerializer.Serialize(settingsToSave, new JsonSerializerOptions { WriteIndented = true }); + lock (_fileIoSync) + { + var configPath = PathManager.WheelWizardConfigFilePath; + try + { + var directoryPath = fileSystem.Path.GetDirectoryName(configPath); + if (!string.IsNullOrWhiteSpace(directoryPath) && !fileSystem.Directory.Exists(directoryPath)) + fileSystem.Directory.CreateDirectory(directoryPath); + + fileSystem.File.WriteAllText(configPath, jsonString); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to save settings file: {Path}", configPath); + } + } + } + + public void LoadSettings() + { + Dictionary settingsSnapshot; + lock (_syncRoot) + { + if (_loaded) + return; + + _loaded = true; + settingsSnapshot = new(_settings); + } + + // Even if it now returns early, loading has been considered complete. + string? jsonString; + lock (_fileIoSync) + { + var configPath = PathManager.WheelWizardConfigFilePath; + try + { + jsonString = fileSystem.File.Exists(configPath) ? fileSystem.File.ReadAllText(configPath) : null; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to read settings file: {Path}", configPath); + jsonString = null; + } + } + + if (jsonString == null) + return; + + try + { + var loadedSettings = JsonSerializer.Deserialize>(jsonString); + if (loadedSettings == null) + return; + + foreach (var kvp in loadedSettings) + { + if (!settingsSnapshot.TryGetValue(kvp.Key, out var setting)) + continue; + + try + { + var success = setting.SetFromJson(kvp.Value, skipSave: true); + if (!success) + setting.Set(setting.DefaultValue, skipSave: true); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Invalid value for setting {SettingName}; resetting to default.", setting.Name); + setting.Set(setting.DefaultValue, skipSave: true); + } + } + } + catch (JsonException e) + { + logger.LogError(e, "Failed to deserialize the JSON config"); + } + } +} diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index 267bd13e..9bcbfb25 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -3,11 +3,11 @@ using System.Text.RegularExpressions; using WheelWizard.Helpers; using WheelWizard.Models.Enums; -using WheelWizard.Models.Settings; using WheelWizard.Services; using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; using WheelWizard.Utilities.Generators; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.WheelWizardData; @@ -89,6 +89,7 @@ public class GameLicenseSingletonService : RepeatedTaskManager, IGameLicenseSing private readonly IFileSystem _fileSystem; private readonly IWhWzDataSingletonService _whWzDataSingletonService; private readonly IRRratingReader _rrratingReader; + private readonly ISettingsManager _settingsManager; private LicenseCollection Licenses { get; } private byte[]? _rksysData; @@ -96,7 +97,8 @@ public GameLicenseSingletonService( IMiiDbService miiService, IFileSystem fileSystem, IWhWzDataSingletonService whWzDataSingletonService, - IRRratingReader rrratingReader + IRRratingReader rrratingReader, + ISettingsManager settingsManager ) : base(40) { @@ -104,6 +106,7 @@ IRRratingReader rrratingReader _fileSystem = fileSystem; _whWzDataSingletonService = whWzDataSingletonService; _rrratingReader = rrratingReader; + _settingsManager = settingsManager; Licenses = new(); } @@ -131,9 +134,9 @@ IRRratingReader rrratingReader /// /// Returns the "focused" or currently active license/user as determined by the Settings. /// - public LicenseProfile ActiveUser => Licenses.Users[(int)SettingsManager.FOCUSSED_USER.Get()]; + public LicenseProfile ActiveUser => Licenses.Users[_settingsManager.Get(_settingsManager.FOCUSED_USER)]; - public List ActiveCurrentFriends => Licenses.Users[(int)SettingsManager.FOCUSSED_USER.Get()].Friends; + public List ActiveCurrentFriends => Licenses.Users[_settingsManager.Get(_settingsManager.FOCUSED_USER)].Friends; public LicenseCollection LicenseCollection => Licenses; @@ -635,7 +638,7 @@ private OperationResult ReadRksys() if (!_fileSystem.Directory.Exists(PathManager.SaveFolderPath)) return Fail("Save folder not found"); - var currentRegion = (MarioKartWiiEnums.Regions)SettingsManager.RR_REGION.Get(); + var currentRegion = _settingsManager.Get(_settingsManager.RR_REGION); if (currentRegion == MarioKartWiiEnums.Regions.None) { // Double check if there's at least one valid region @@ -643,7 +646,7 @@ private OperationResult ReadRksys() if (validRegions.First() != MarioKartWiiEnums.Regions.None) { currentRegion = validRegions.First(); - SettingsManager.RR_REGION.Set(currentRegion); + _settingsManager.Set(_settingsManager.RR_REGION, currentRegion); } else { @@ -753,10 +756,10 @@ private OperationResult WriteLicenseNameToSaveData(int userIndex, string newName private OperationResult SaveRksysToFile() { - if (_rksysData == null || !SettingsHelper.PathsSetupCorrectly()) + if (_rksysData == null || !_settingsManager.PathsSetupCorrectly()) return Fail("Invalid save data or config is not setup properly."); FixRksysCrc(_rksysData); - var currentRegion = (MarioKartWiiEnums.Regions)SettingsManager.RR_REGION.Get(); + var currentRegion = _settingsManager.Get(_settingsManager.RR_REGION); var saveFolder = _fileSystem.Path.Combine(PathManager.SaveFolderPath, RRRegionManager.ConvertRegionToGameId(currentRegion)); var trySaveRksys = TryCatch(() => { diff --git a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs index 0ef4fff4..82e85d46 100644 --- a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs +++ b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs @@ -1,10 +1,12 @@ -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; namespace WheelWizard.WiiManagement.MiiManagement; public static class MiiExtensions { + private static ISettingsManager Settings => SettingsRuntime.Current; + private static readonly DateTime MiiIdEpochUtc = new(2006, 1, 1, 0, 0, 0, DateTimeKind.Utc); private const uint MiiIdCounterMask = 0x1FFFFFFF; private const int MiiIdTickResolutionSeconds = 4; @@ -70,7 +72,7 @@ public static bool IsGlobal(this Mii self) return true; // But it can also be global if the mac address is not the same as your own address - var macAddressString = (string)SettingsManager.MACADDRESS.Get(); + var macAddressString = Settings.Get(Settings.MACADDRESS); var macParts = macAddressString.Split(':'); var macBytes = new byte[6]; for (var i = 0; i < 6; i++) diff --git a/WheelWizard/Helpers/FileHelper.cs b/WheelWizard/Helpers/FileHelper.cs index 934ffdb5..2a2c8700 100644 --- a/WheelWizard/Helpers/FileHelper.cs +++ b/WheelWizard/Helpers/FileHelper.cs @@ -57,6 +57,7 @@ public DirectoryMoveContentsResult( // From now on we to have this FileHelper as a middle man whenever we do anything file related. This makes // it easier to create helper methods, mock data, and most importantly, easy to make it multi-platform later on +[Obsolete("FileHelper is deprecated. Use IFileSystem and feature-specific services instead.")] public static class FileHelper { public static bool FileExists(string path) => File.Exists(path); diff --git a/WheelWizard/Models/Settings/Mod.cs b/WheelWizard/Models/Mods/Mod.cs similarity index 98% rename from WheelWizard/Models/Settings/Mod.cs rename to WheelWizard/Models/Mods/Mod.cs index df9e65f1..975b6fc1 100644 --- a/WheelWizard/Models/Settings/Mod.cs +++ b/WheelWizard/Models/Mods/Mod.cs @@ -3,7 +3,7 @@ using IniParser; using IniParser.Model; -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Models.Mods; public class Mod : INotifyPropertyChanged { diff --git a/WheelWizard/Models/Settings/ISettingListener.cs b/WheelWizard/Models/Settings/ISettingListener.cs deleted file mode 100644 index 2ee17443..00000000 --- a/WheelWizard/Models/Settings/ISettingListener.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WheelWizard.Models.Settings; - -public interface ISettingListener -{ - public void OnSettingChanged(Setting setting); -} diff --git a/WheelWizard/Models/Settings/ListedSetting.cs b/WheelWizard/Models/Settings/ListedSetting.cs deleted file mode 100644 index 7b6a4256..00000000 --- a/WheelWizard/Models/Settings/ListedSetting.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace WheelWizard.Models.Settings; - -public class ListedSetting -{ - public readonly Dictionary Mapping = new(); - public readonly List AllKeys = []; - public readonly List AllValues = []; - public T DefaultValue { get; set; } - - public ListedSetting(string defaultKey, params (string, T)[] values) - { - foreach (var (key, value) in values) - { - Mapping[key] = value; - } - AllKeys.AddRange(Mapping.Keys); - AllValues.AddRange(Mapping.Values); - DefaultValue = Mapping[defaultKey]; - } - - public T Get(string key) => Mapping[key]; -} diff --git a/WheelWizard/Program.cs b/WheelWizard/Program.cs index 6eaa931c..4f0f8e36 100644 --- a/WheelWizard/Program.cs +++ b/WheelWizard/Program.cs @@ -3,8 +3,8 @@ using Avalonia.Logging; using Serilog; using WheelWizard.Helpers; -using WheelWizard.Services.Settings; using WheelWizard.Services.UrlProtocol; +using WheelWizard.Settings; using WheelWizard.Shared.Services; using WheelWizard.Views; @@ -69,7 +69,7 @@ private static AppBuilder CreateWheelWizardApp(bool isDesigner) // Make sure this comes AFTER setting the service provider // of the `App` instance! Otherwise, things like logging will not work // in `Setup`. - Setup(); + Setup(serviceProvider); }); return builder; @@ -106,9 +106,9 @@ private static void SetupWorkingDirectory() } } - private static void Setup() + private static void Setup(IServiceProvider serviceProvider) { - SettingsManager.Instance.LoadSettings(); + serviceProvider.GetRequiredService().Initialize(); UrlProtocolManager.SetWhWzScheme(); } } diff --git a/WheelWizard/Services/Installation/ModInstallation.cs b/WheelWizard/Services/Installation/ModInstallation.cs index e4a25ec6..35250358 100644 --- a/WheelWizard/Services/Installation/ModInstallation.cs +++ b/WheelWizard/Services/Installation/ModInstallation.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Avalonia.Threading; using SharpCompress.Archives; -using WheelWizard.Models.Settings; +using WheelWizard.Models.Mods; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Services.Installation; diff --git a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs index bb4989fb..12dca25a 100644 --- a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs +++ b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs @@ -2,13 +2,15 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Services.Launcher.Helpers; public static class DolphinLaunchHelper { + private static ISettingsManager Settings => SettingsRuntime.Current; + public static void KillDolphin() //dont tell PETA { var dolphinLocation = PathManager.DolphinFilePath; @@ -130,7 +132,7 @@ public static void LaunchDolphin(string arguments = "", bool shellExecute = fals var userFolderArgument = cannotPassUserFolder ? "" : $"-u {EnvHelper.QuotePath(Path.GetFullPath(PathManager.UserFolderPath))}"; var dolphinLaunchArguments = $"{arguments} {userFolderArgument}"; - var dolphinLocation = (string)SettingsManager.DOLPHIN_LOCATION.Get(); + var dolphinLocation = Settings.Get(Settings.DOLPHIN_LOCATION); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Windows builds diff --git a/WheelWizard/Services/Launcher/RrBetaLauncher.cs b/WheelWizard/Services/Launcher/RrBetaLauncher.cs index 1bcba6b9..c7c0066d 100644 --- a/WheelWizard/Services/Launcher/RrBetaLauncher.cs +++ b/WheelWizard/Services/Launcher/RrBetaLauncher.cs @@ -4,9 +4,8 @@ using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services.Launcher.Helpers; -using WheelWizard.Services.Settings; using WheelWizard.Services.WiiManagement; -using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Settings; using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; @@ -16,10 +15,14 @@ public class RrBetaLauncher : ILauncher { public string GameTitle { get; } = "Retro Rewind Beta"; private static string RrLaunchJsonFilePath => PathManager.RrLaunchJsonFilePath; + private readonly ICustomDistributionSingletonService _customDistributionSingletonService; + private readonly ISettingsManager _settingsManager; - [Inject] - private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = - App.Services.GetRequiredService(); + public RrBetaLauncher(ICustomDistributionSingletonService customDistributionSingletonService, ISettingsManager settingsManager) + { + _customDistributionSingletonService = customDistributionSingletonService; + _settingsManager = settingsManager; + } public async Task Launch() { @@ -43,7 +46,7 @@ public async Task Launch() } RetroRewindLaunchHelper.GenerateLaunchJson(PathManager.RrBetaXmlFilePath); - var dolphinLaunchType = (bool)SettingsManager.LAUNCH_WITH_DOLPHIN.Get() ? "" : "-b"; + var dolphinLaunchType = _settingsManager.Get(_settingsManager.LAUNCH_WITH_DOLPHIN) ? "" : "-b"; DolphinLaunchHelper.LaunchDolphin( $"{dolphinLaunchType} -e {EnvHelper.QuotePath(Path.GetFullPath(RrLaunchJsonFilePath))} --config=Dolphin.Core.EnableCheats=False --config=Achievements.Achievements.Enabled=False" ); @@ -65,7 +68,7 @@ public async Task Install() { var progressWindow = new ProgressWindow("Installing test build"); progressWindow.Show(); - var installResult = await CustomDistributionSingletonService.RetroRewindBeta.InstallAsync(progressWindow); + var installResult = await _customDistributionSingletonService.RetroRewindBeta.InstallAsync(progressWindow); progressWindow.Close(); if (installResult.IsFailure) { @@ -81,17 +84,13 @@ public async Task Update() { var progressWindow = new ProgressWindow("Updating test build"); progressWindow.Show(); - await CustomDistributionSingletonService.RetroRewindBeta.UpdateAsync(progressWindow); + await _customDistributionSingletonService.RetroRewindBeta.UpdateAsync(progressWindow); progressWindow.Close(); } public async Task GetCurrentStatus() { - if (CustomDistributionSingletonService == null) - { - return WheelWizardStatus.NotInstalled; - } - var statusResult = await CustomDistributionSingletonService.RetroRewindBeta.GetCurrentStatusAsync(); + var statusResult = await _customDistributionSingletonService.RetroRewindBeta.GetCurrentStatusAsync(); if (statusResult.IsFailure) return WheelWizardStatus.NotInstalled; return statusResult.Value; diff --git a/WheelWizard/Services/Launcher/RrLauncher.cs b/WheelWizard/Services/Launcher/RrLauncher.cs index 9a18eb36..ec08959a 100644 --- a/WheelWizard/Services/Launcher/RrLauncher.cs +++ b/WheelWizard/Services/Launcher/RrLauncher.cs @@ -5,9 +5,8 @@ using WheelWizard.Resources.Languages; using WheelWizard.Services.Installation; using WheelWizard.Services.Launcher.Helpers; -using WheelWizard.Services.Settings; using WheelWizard.Services.WiiManagement; -using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Settings; using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; @@ -17,10 +16,14 @@ public class RrLauncher : ILauncher { public string GameTitle { get; } = "Retro Rewind"; private static string RrLaunchJsonFilePath => PathManager.RrLaunchJsonFilePath; + private readonly ICustomDistributionSingletonService _customDistributionSingletonService; + private readonly ISettingsManager _settingsManager; - [Inject] - private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = - App.Services.GetRequiredService(); + public RrLauncher(ICustomDistributionSingletonService customDistributionSingletonService, ISettingsManager settingsManager) + { + _customDistributionSingletonService = customDistributionSingletonService; + _settingsManager = settingsManager; + } public async Task Launch() { @@ -44,7 +47,7 @@ public async Task Launch() } RetroRewindLaunchHelper.GenerateLaunchJson(); - var dolphinLaunchType = (bool)SettingsManager.LAUNCH_WITH_DOLPHIN.Get() ? "" : "-b"; + var dolphinLaunchType = _settingsManager.Get(_settingsManager.LAUNCH_WITH_DOLPHIN) ? "" : "-b"; DolphinLaunchHelper.LaunchDolphin( $"{dolphinLaunchType} -e {EnvHelper.QuotePath(Path.GetFullPath(RrLaunchJsonFilePath))} --config=Dolphin.Core.EnableCheats=False --config=Achievements.Achievements.Enabled=False" ); @@ -66,7 +69,7 @@ public async Task Install() { var progressWindow = new ProgressWindow(); progressWindow.Show(); - var installResult = await CustomDistributionSingletonService.RetroRewind.InstallAsync(progressWindow); + var installResult = await _customDistributionSingletonService.RetroRewind.InstallAsync(progressWindow); progressWindow.Close(); if (installResult.IsFailure) { @@ -82,17 +85,13 @@ public async Task Update() { var progressWindow = new ProgressWindow(); progressWindow.Show(); - await CustomDistributionSingletonService.RetroRewind.UpdateAsync(progressWindow); + await _customDistributionSingletonService.RetroRewind.UpdateAsync(progressWindow); progressWindow.Close(); } public async Task GetCurrentStatus() { - if (CustomDistributionSingletonService == null) - { - return WheelWizardStatus.NotInstalled; - } - var statusResult = await CustomDistributionSingletonService.RetroRewind.GetCurrentStatusAsync(); + var statusResult = await _customDistributionSingletonService.RetroRewind.GetCurrentStatusAsync(); if (statusResult.IsFailure) return WheelWizardStatus.NotInstalled; return statusResult.Value; diff --git a/WheelWizard/Services/LiveData/RRLiveRooms.cs b/WheelWizard/Services/LiveData/RRLiveRooms.cs index 99aef535..b9a50c08 100644 --- a/WheelWizard/Services/LiveData/RRLiveRooms.cs +++ b/WheelWizard/Services/LiveData/RRLiveRooms.cs @@ -11,24 +11,32 @@ namespace WheelWizard.Services.LiveData; public class RRLiveRooms : RepeatedTaskManager { + private readonly IWhWzDataSingletonService _whWzService; + private readonly IRrRoomsSingletonService _roomsService; + private readonly IRrLeaderboardSingletonService _leaderboardService; + public List CurrentRooms { get; private set; } = []; public int PlayerCount => CurrentRooms.Sum(room => room.PlayerCount); public int RoomCount => CurrentRooms.Count; - private static RRLiveRooms? _instance; - public static RRLiveRooms Instance => _instance ??= new(); + public static RRLiveRooms Instance => App.Services.GetRequiredService(); - private RRLiveRooms() - : base(40) { } + public RRLiveRooms( + IWhWzDataSingletonService whWzService, + IRrRoomsSingletonService roomsService, + IRrLeaderboardSingletonService leaderboardService + ) + : base(40) + { + _whWzService = whWzService; + _roomsService = roomsService; + _leaderboardService = leaderboardService; + } protected override async Task ExecuteTaskAsync() { - var whWzService = App.Services.GetRequiredService(); - var roomsService = App.Services.GetRequiredService(); - var leaderboardService = App.Services.GetRequiredService(); - - var roomsTask = roomsService.GetRoomsAsync(); - var leaderboardTask = leaderboardService.GetTopPlayersAsync(50); + var roomsTask = _roomsService.GetRoomsAsync(); + var leaderboardTask = _leaderboardService.GetTopPlayersAsync(50); await Task.WhenAll(roomsTask, leaderboardTask); @@ -59,7 +67,7 @@ protected override async Task ExecuteTaskAsync() var raw = roomsResult.Value; var splitRaw = SplitMergedRooms(raw); - var rrRooms = splitRaw.Select(room => MapRoom(room, whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList(); + var rrRooms = splitRaw.Select(room => MapRoom(room, _whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList(); CurrentRooms = rrRooms; } diff --git a/WheelWizard/Services/LiveData/WhWzStatusManager.cs b/WheelWizard/Services/LiveData/WhWzStatusManager.cs index 9e59ff03..e8078368 100644 --- a/WheelWizard/Services/LiveData/WhWzStatusManager.cs +++ b/WheelWizard/Services/LiveData/WhWzStatusManager.cs @@ -8,18 +8,23 @@ namespace WheelWizard.Services.LiveData; public class WhWzStatusManager : RepeatedTaskManager { + private readonly IWhWzDataSingletonService _whWzDataService; + private readonly ILogger _logger; + public WhWzStatus? Status { get; private set; } - private static WhWzStatusManager? _instance; - public static WhWzStatusManager Instance => _instance ??= new(); + public static WhWzStatusManager Instance => App.Services.GetRequiredService(); - private WhWzStatusManager() - : base(90) { } + public WhWzStatusManager(IWhWzDataSingletonService whWzDataService, ILogger logger) + : base(90) + { + _whWzDataService = whWzDataService; + _logger = logger; + } protected override async Task ExecuteTaskAsync() { - var whWzDataService = App.Services.GetRequiredService(); - var statusResult = await whWzDataService.GetStatusAsync(); + var statusResult = await _whWzDataService.GetStatusAsync(); if (statusResult.IsSuccess) { @@ -27,8 +32,7 @@ protected override async Task ExecuteTaskAsync() return; } - App.Services.GetRequiredService>() - .LogError(statusResult.Error.Exception, "Failed to retrieve WhWz Status: {Message}", statusResult.Error.Message); + _logger.LogError(statusResult.Error.Exception, "Failed to retrieve WhWz Status: {Message}", statusResult.Error.Message); Status = new() { Variant = WhWzStatusVariant.Error, Message = "Failed to retrieve Wheel Wizard status" }; } } diff --git a/WheelWizard/Services/Settings/ModConfigManager.cs b/WheelWizard/Services/ModConfigManager.cs similarity index 100% rename from WheelWizard/Services/Settings/ModConfigManager.cs rename to WheelWizard/Services/ModConfigManager.cs diff --git a/WheelWizard/Services/ModManager.cs b/WheelWizard/Services/ModManager.cs index 534a8ac3..98e36123 100644 --- a/WheelWizard/Services/ModManager.cs +++ b/WheelWizard/Services/ModManager.cs @@ -5,7 +5,7 @@ using Avalonia.Threading; using IniParser; using WheelWizard.Helpers; -using WheelWizard.Models.Settings; +using WheelWizard.Models.Mods; using WheelWizard.Resources.Languages; using WheelWizard.Services.Installation; using WheelWizard.Views.Popups.Generic; diff --git a/WheelWizard/Services/PathManager.cs b/WheelWizard/Services/PathManager.cs index 4417cd1f..1719332b 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; #if WINDOWS using Microsoft.Win32; #endif @@ -9,6 +9,8 @@ namespace WheelWizard.Services; public static class PathManager { + private static ISettingsManager Settings => SettingsRuntime.Current; + // IMPORTANT: To keep things consistent all paths should be Attrib expressions, // and either end with `FilePath` or `FolderPath` @@ -34,9 +36,9 @@ static PathManager() public static string HomeFolderPath => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // Paths set by the user - public static string GameFilePath => (string)SettingsManager.GAME_LOCATION.Get(); - public static string DolphinFilePath => (string)SettingsManager.DOLPHIN_LOCATION.Get(); - public static string UserFolderPath => (string)SettingsManager.USER_FOLDER_PATH.Get(); + public static string GameFilePath => Settings.Get(Settings.GAME_LOCATION); + public static string DolphinFilePath => Settings.Get(Settings.DOLPHIN_LOCATION); + public static string UserFolderPath => Settings.Get(Settings.USER_FOLDER_PATH); private static string AppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); private static string LocalAppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -603,9 +605,9 @@ public static string LoadFolderPath { get { - if (SettingsManager.LOAD_PATH.IsValid()) + if (Settings.LOAD_PATH.IsValid()) { - return (string)SettingsManager.LOAD_PATH.Get(); + return Settings.Get(Settings.LOAD_PATH); } return Path.Combine(UserFolderPath, "Load"); } @@ -637,9 +639,9 @@ public static string WiiFolderPath { get { - if (SettingsManager.NAND_ROOT_PATH.IsValid()) + if (Settings.NAND_ROOT_PATH.IsValid()) { - return (string)SettingsManager.NAND_ROOT_PATH.Get(); + return Settings.Get(Settings.NAND_ROOT_PATH); } return Path.Combine(UserFolderPath, "Wii"); } diff --git a/WheelWizard/Services/Settings/DolphinSettingManager.cs b/WheelWizard/Services/Settings/DolphinSettingManager.cs deleted file mode 100644 index 9e11bffd..00000000 --- a/WheelWizard/Services/Settings/DolphinSettingManager.cs +++ /dev/null @@ -1,138 +0,0 @@ -using WheelWizard.Helpers; -using WheelWizard.Models.Settings; - -namespace WheelWizard.Services.Settings; - -public class DolphinSettingManager -{ - private static string ConfigFolderPath(string fileName) => Path.Combine(PathManager.ConfigFolderPath, fileName); - - private bool _loaded; - private readonly List _settings = []; - - public static DolphinSettingManager Instance { get; } = new(); - - private DolphinSettingManager() { } - - public void RegisterSetting(DolphinSetting setting) - { - if (_loaded) - return; - - _settings.Add(setting); - } - - public void SaveSettings(DolphinSetting invokingSetting) - { - // TODO: This method definitely has to be optimized - if (!_loaded) - return; - - foreach (var setting in _settings) - { - ChangeIniSettings(setting.FileName, setting.Section, setting.Name, setting.GetStringValue()); - } - } - - public void ReloadSettings() - { - // TODO: this method could also be optimized by checking if the previously loaded directory - // is still the current ConfigFolderPath and if so, just not run the LoadSettings method again - _loaded = false; - LoadSettings(); - } - - public void LoadSettings() - { - if (_loaded) - return; - - if (!FileHelper.DirectoryExists(PathManager.ConfigFolderPath)) - return; - - // TODO: This method can maybe be optimized in the future, since now it reads the file for every setting - // and on top of that for reach setting it loops over each line and section and stuff like that. - foreach (var setting in _settings) - { - var value = ReadIniSetting(setting.FileName, setting.Section, setting.Name); - if (value == null) - ChangeIniSettings(setting.FileName, setting.Section, setting.Name, setting.GetStringValue()); - else - setting.SetFromString(value, true); // we read it, which means there is no purpose in saving it again - } - - _loaded = true; - } - - private static string[]? ReadIniFile(string fileName) - { - var filePath = ConfigFolderPath(fileName); - var lines = FileHelper.ReadAllLinesSafe(filePath); - return lines; - } - - private static string? ReadIniSetting(string fileName, string section, string settingToRead) - { - var lines = ReadIniFile(fileName); - if (lines == null) - return null; - - var sectionIndex = Array.IndexOf(lines, $"[{section}]"); - if (sectionIndex == -1) - return null; - - // find all the settings related to this section, we dont want to read/influence other sections - var nextSectionName = lines.Skip(sectionIndex + 1).FirstOrDefault(x => x.Trim().StartsWith("[") && x.Trim().EndsWith("]")); - var nextSectionIndex = Array.IndexOf(lines, nextSectionName); - var sectionLines = lines.Skip(sectionIndex + 1); - if (nextSectionIndex != -1) - sectionLines = sectionLines.Take(nextSectionIndex - sectionIndex - 1); - - // finally we can read the setting - foreach (var line in sectionLines) - { - if (!line.StartsWith($"{settingToRead}=") && !line.StartsWith($"{settingToRead} =")) - continue; - //we found the setting, now we need to return the value - var setting = line.Split("="); - return setting[1].Trim(); - } - - return null; - } - - // TODO: find out when to use `setting=value` and when to use `setting = value` - private static void ChangeIniSettings(string fileName, string section, string settingToChange, string value) - { - var lines = ReadIniFile(fileName)?.ToList(); - if (lines == null) - return; - - var sectionIndex = lines.IndexOf($"[{section}]"); - if (sectionIndex == -1) - { - lines.Add($"[{section}]"); - lines.Add($"{settingToChange} = {value}"); - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); - return; - } - - for (var i = sectionIndex + 1; i < lines.Count; i++) - { - // - if (lines[i].Trim().StartsWith("[") && lines[i].Trim().EndsWith("]")) - break; // Setting was not found in this section, so we have to append it to the section - - if (!lines[i].StartsWith($"{settingToChange}=") && !lines[i].StartsWith($"{settingToChange} =")) - continue; - - lines[i] = $"{settingToChange} = {value}"; - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); - return; - } - // you only get here if the setting was not found in the section - - lines.Insert(sectionIndex + 1, $"{settingToChange} = {value}"); - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); - } -} diff --git a/WheelWizard/Services/Settings/LinuxDolphinInstaller.cs b/WheelWizard/Services/Settings/LinuxDolphinInstaller.cs deleted file mode 100644 index e8e02aad..00000000 --- a/WheelWizard/Services/Settings/LinuxDolphinInstaller.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; -using Avalonia.Threading; -using WheelWizard.Helpers; -using WheelWizard.Views.Popups.Generic; - -namespace WheelWizard.Services.Settings; - -public static class LinuxDolphinInstaller -{ - public static bool IsDolphinInstalledInFlatpak() - { - try - { - var processInfo = new ProcessStartInfo - { - FileName = "flatpak", - Arguments = "info org.DolphinEmu.dolphin-emu", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - using var process = Process.Start(processInfo); - process.WaitForExit(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } - - // Helper method to run a process asynchronously with progress reporting. - private static async Task RunProcessWithProgressAsync(string fileName, string arguments, IProgress progress = null) - { - var processInfo = new ProcessStartInfo - { - FileName = fileName, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - using var process = Process.Start(processInfo); - - // Listen for output data to parse progress. - process.OutputDataReceived += (sender, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) - return; - - var match = Regex.Match(e.Data, @"(\d+)%"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int percent)) - { - progress?.Report(percent); - } - }; - - process.ErrorDataReceived += (sender, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) - return; - - var match = Regex.Match(e.Data, @"(\d+)%"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int percent)) - { - progress?.Report(percent); - } - }; - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - await process.WaitForExitAsync(); - return process.ExitCode; - } - - /// - /// Checks if Flatpak is installed by verifying the command exists. - /// - /// True if Flatpak is available; otherwise, false. - public static bool isFlatPakInstalled() - { - return EnvHelper.IsValidUnixCommand("flatpak"); - } - - /// - /// Installs Flatpak - /// Reports progress via the provided IProgress callback. - /// - /// True if installation succeeded; otherwise, false. - public static async Task InstallFlatpak(IProgress progress = null) - { - if (isFlatPakInstalled()) - return true; - - // Detect the package manager - var packageManagerCommand = EnvHelper.DetectLinuxPackageManagerInstallCommand(); - if (string.IsNullOrEmpty(packageManagerCommand)) - return false; // Unsupported distro - - // Install Flatpak - var exitCode = await RunProcessWithProgressAsync("pkexec", $"{packageManagerCommand} flatpak", progress); - if (exitCode is 127 or 126) //this is error unauthorized - { - Dispatcher.UIThread.InvokeAsync(() => - { - new MessageBoxWindow().SetTitleText("Error").SetTitleText("You need to be an administrator to install Flatpak").Show(); - }); - } - - return exitCode == 0 && isFlatPakInstalled(); - } - - /// - /// Installs Dolphin via Flatpak. - /// Ensures that Flatpak is installed and reports progress via the provided IProgress callback. - /// - /// True if Dolphin was successfully installed; otherwise, false. - public static async Task InstallFlatpakDolphin(IProgress progress = null) - { - // Ensure Flatpak is installed; if not, attempt installation. - if (!isFlatPakInstalled()) - { - var flatpakInstalled = await InstallFlatpak(progress); - if (!flatpakInstalled) - return false; - } - - // Install Dolphin using Flatpak and report progress. - var exitCode = await RunProcessWithProgressAsync("pkexec", "flatpak --system install -y org.DolphinEmu.dolphin-emu", progress); - if (exitCode is 127 or 126) //this is error unauthorized - { - Dispatcher.UIThread.InvokeAsync(() => - { - new MessageBoxWindow() - .SetTitleText("Error") - .SetTitleText("You need to be an administrator to install Dolphin via Flatpak") - .Show(); - }); - return false; - } - - if (exitCode != 0) - return false; - try - { - var dolphinProcess = new Process - { - StartInfo = new() - { - FileName = "flatpak", - Arguments = "run org.DolphinEmu.dolphin-emu", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }, - }; - - dolphinProcess.Start(); - await Task.Delay(4000); - dolphinProcess.Kill(); - } - catch (Exception ex) - { - Dispatcher.UIThread.InvokeAsync(() => - { - new MessageBoxWindow().SetTitleText("Error").SetTitleText($"Failed to launch Dolphin").SetInfoText(ex.Message).Show(); - }); - } - - return true; - } -} diff --git a/WheelWizard/Services/Settings/SettingsHelper.cs b/WheelWizard/Services/Settings/SettingsHelper.cs deleted file mode 100644 index 8780bb38..00000000 --- a/WheelWizard/Services/Settings/SettingsHelper.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Globalization; -using WheelWizard.Models.Settings; - -namespace WheelWizard.Services.Settings; - -// This class is meant for all the loose little helper methods regarding settings. -public class SettingsHelper : ISettingListener -{ - private SettingsHelper() { } - - private static readonly SettingsHelper Instance = new(); - - public static void LoadExtraStuff() - { - SettingsManager.WW_LANGUAGE.Subscribe(Instance); - Instance.OnWheelWizardLanguageChange(); - } - - public static bool PathsSetupCorrectly() - { - return SettingsManager.USER_FOLDER_PATH.IsValid() - && SettingsManager.DOLPHIN_LOCATION.IsValid() - && SettingsManager.GAME_LOCATION.IsValid(); - } - - public void OnSettingChanged(Setting setting) - { - if (setting == SettingsManager.WW_LANGUAGE) - OnWheelWizardLanguageChange(); - } - - private void OnWheelWizardLanguageChange() - { - var lang = (string)SettingsManager.WW_LANGUAGE.Get(); - var newCulture = new CultureInfo(lang); - CultureInfo.CurrentCulture = newCulture; - CultureInfo.CurrentUICulture = newCulture; - } -} diff --git a/WheelWizard/Services/Settings/SettingsManager.cs b/WheelWizard/Services/Settings/SettingsManager.cs deleted file mode 100644 index bbdc61e6..00000000 --- a/WheelWizard/Services/Settings/SettingsManager.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System.Runtime.InteropServices; -using WheelWizard.Helpers; -using WheelWizard.Models.Enums; -using WheelWizard.Models.Settings; - -namespace WheelWizard.Services.Settings; - -public class SettingsManager -{ - #region Wheel Wizard Settings - public static Setting USER_FOLDER_PATH = new WhWzSetting(typeof(string), "UserFolderPath", "").SetValidation(value => - { - var userFolderPath = value as string ?? string.Empty; - if (!FileHelper.DirectoryExists(userFolderPath)) - return false; - - string dolphinLocation = DOLPHIN_LOCATION.Get() as string ?? string.Empty; - - // We cannot determine the validity of the user folder path in that case - if (string.IsNullOrWhiteSpace(dolphinLocation)) - return true; - - // If we want to use a split XDG dolphin config, - // this only really works as expected if certain conditions are met - // (we cannot simply pass `-u` to Dolphin since that would put the `Config` directory - // inside the data directory and not use the XDG config directory, leading to two different configs). - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && PathManager.IsLinuxDolphinConfigSplit()) - { - // In this case, Dolphin would use `EMBEDDED_USER_DIR` which is the portable `user` directory - // in the current directory (the directory of the WheelWizard executable). - // This means a split dolphin user folder and config cannot work... - if (FileHelper.DirectoryExists("user")) - return false; - - // The Dolphin executable directory with `portable.txt` case - if (FileHelper.FileExists(Path.Combine(PathManager.GetDolphinExeDirectory(), "portable.txt"))) - return false; - - // The value of this environment variable would be used instead if it was somehow set - string environmentVariableToAvoid = "DOLPHIN_EMU_USERPATH"; - - if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(environmentVariableToAvoid))) - return false; - - if (dolphinLocation.Contains(environmentVariableToAvoid, StringComparison.Ordinal)) - return false; - - // `~/.dolphin-emu` would be used if it exists - if (!PathManager.IsFlatpakDolphinFilePath() && FileHelper.DirectoryExists(PathManager.LinuxDolphinLegacyFolderPath)) - return false; - } - - return true; - }); - - public static Setting DOLPHIN_LOCATION = new WhWzSetting(typeof(string), "DolphinLocation", "").SetValidation(value => - { - var pathOrCommand = value as string ?? string.Empty; - if (string.IsNullOrWhiteSpace(pathOrCommand)) - return false; - - if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !LinuxDolphinInstaller.IsDolphinInstalledInFlatpak()) - { - return false; - } - } - return EnvHelper.IsValidUnixCommand(pathOrCommand); - } - - return FileHelper.FileExists(pathOrCommand); - }); - - public static Setting GAME_LOCATION = new WhWzSetting(typeof(string), "GameLocation", "").SetValidation(value => - FileHelper.FileExists(value as string ?? string.Empty) - ); - public static Setting FORCE_WIIMOTE = new WhWzSetting(typeof(bool), "ForceWiimote", false); - public static Setting LAUNCH_WITH_DOLPHIN = new WhWzSetting(typeof(bool), "LaunchWithDolphin", false); - public static Setting PREFERS_MODS_ROW_VIEW = new WhWzSetting(typeof(bool), "PrefersModsRowView", true); - public static Setting FOCUSSED_USER = new WhWzSetting(typeof(int), "FavoriteUser", 0).SetValidation(value => - (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4 - ); - - public static Setting ENABLE_ANIMATIONS = new WhWzSetting(typeof(bool), "EnableAnimations", true); - public static Setting TESTING_MODE_ENABLED = new WhWzSetting(typeof(bool), "TestingModeEnabled", false); - public static Setting SAVED_WINDOW_SCALE = new WhWzSetting(typeof(double), "WindowScale", 1.0).SetValidation(value => - (double)(value ?? -1) >= 0.5 && (double)(value ?? -1) <= 2.0 - ); - public static Setting REMOVE_BLUR = new WhWzSetting(typeof(bool), "REMOVE_BLUR", true); - public static Setting RR_REGION = new WhWzSetting(typeof(MarioKartWiiEnums.Regions), "RR_Region", MarioKartWiiEnums.Regions.None); - public static Setting WW_LANGUAGE = new WhWzSetting(typeof(string), "WW_Language", "en").SetValidation(value => - SettingValues.WhWzLanguages.ContainsKey((string)value!) - ); - #endregion - - #region Dolphin Settings - public static Setting NAND_ROOT_PATH = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "").SetValidation( - value => Directory.Exists(value as string ?? string.Empty) - ); - - public static Setting LOAD_PATH = new DolphinSetting(typeof(string), ("Dolphin.ini", "General", "LoadPath"), "").SetValidation(value => - Directory.Exists(value as string ?? string.Empty) - ); - - public static Setting VSYNC = new DolphinSetting(typeof(bool), ("GFX.ini", "Hardware", "VSync"), false); - public static Setting INTERNAL_RESOLUTION = new DolphinSetting( - typeof(int), - ("GFX.ini", "Settings", "InternalResolution"), - 1 - ).SetValidation(value => (int)(value ?? -1) >= 0); - public static Setting SHOW_FPS = new DolphinSetting(typeof(bool), ("GFX.ini", "Settings", "ShowFPS"), false); - public static Setting GFX_BACKEND = new DolphinSetting( - typeof(string), - ("Dolphin.ini", "Core", "GFXBackend"), - SettingValues.GFXRenderers.Values.First() - ); - - //recommended settings - private static Setting DOLPHIN_COMPILATION_MODE = new DolphinSetting( - typeof(DolphinShaderCompilationMode), - ("GFX.ini", "Settings", "ShaderCompilationMode"), - DolphinShaderCompilationMode.Default - ); - private static Setting DOLPHIN_COMPILE_SHADERS_AT_START = new DolphinSetting( - typeof(bool), - ("GFX.ini", "Settings", "WaitForShadersBeforeStarting"), - false - ); - private static Setting DOLPHIN_SSAA = new DolphinSetting(typeof(bool), ("GFX.ini", "Settings", "SSAA"), false); - private static Setting DOLPHIN_MSAA = new DolphinSetting(typeof(string), ("GFX.ini", "Settings", "MSAA"), "0x00000001").SetValidation( - value => (value?.ToString() ?? "") is "0x00000001" or "0x00000002" or "0x00000004" or "0x00000008" - ); - - //Readonly settings - public static readonly Setting MACADDRESS = new DolphinSetting( - typeof(string), - ("Dolphin.ini", "General", "WirelessMac"), - "02:01:02:03:04:05" - ); - #endregion - - #region Virtual Settings - private static double _internalScale = -1.0; - public static Setting WINDOW_SCALE = new VirtualSetting( - typeof(double), - value => _internalScale = (double)value!, - () => _internalScale == -1.0 ? SAVED_WINDOW_SCALE.Get() : _internalScale - ).SetDependencies(SAVED_WINDOW_SCALE); - - public static Setting RECOMMENDED_SETTINGS = new VirtualSetting( - typeof(bool), - value => - { - var newValue = (bool)value!; - DOLPHIN_COMPILATION_MODE.Set(newValue ? DolphinShaderCompilationMode.HybridUberShaders : DolphinShaderCompilationMode.Default); -#if WINDOWS - DOLPHIN_COMPILE_SHADERS_AT_START.Set(newValue); -#endif - DOLPHIN_MSAA.Set(newValue ? "0x00000002" : "0x00000001"); - DOLPHIN_SSAA.Set(false); - }, - () => - { - var value1 = (DolphinShaderCompilationMode)DOLPHIN_COMPILATION_MODE.Get(); - var value2 = true; -#if WINDOWS - value2 = (bool)DOLPHIN_COMPILE_SHADERS_AT_START.Get(); -#endif - var value3 = (string)DOLPHIN_MSAA.Get(); - var value4 = (bool)DOLPHIN_SSAA.Get(); - return !value4 && value2 && value3 == "0x00000002" && value1 == DolphinShaderCompilationMode.HybridUberShaders; - } - ).SetDependencies(DOLPHIN_COMPILATION_MODE, DOLPHIN_COMPILE_SHADERS_AT_START, DOLPHIN_MSAA, DOLPHIN_SSAA); - - private static RrGameMode _internalRrGameMode = RrGameMode.RETRO_TRACKS; - #endregion - - - #region Base Settings Manager - // dont ever make this a static class, it is required to be an instance class to ensure all settings are loaded - public static SettingsManager Instance { get; } = new(); - - private SettingsManager() { } - - // dont make this a static method - public void LoadSettings() - { - WhWzSettingManager.Instance.LoadSettings(); - DolphinSettingManager.Instance.LoadSettings(); - SettingsHelper.LoadExtraStuff(); - } - #endregion -} diff --git a/WheelWizard/Services/Settings/WhWzSettingManager.cs b/WheelWizard/Services/Settings/WhWzSettingManager.cs deleted file mode 100644 index df6fa3d3..00000000 --- a/WheelWizard/Services/Settings/WhWzSettingManager.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.Logging; -using WheelWizard.Helpers; -using WheelWizard.Models.Settings; -using WheelWizard.Views; -using JsonElement = System.Text.Json.JsonElement; -using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; - -namespace WheelWizard.Services.Settings; - -public class WhWzSettingManager -{ - private bool _loaded; - private readonly Dictionary _settings = new(); - - public static WhWzSettingManager Instance { get; } = new(); - - private WhWzSettingManager() { } - - public void RegisterSetting(WhWzSetting setting) - { - if (_loaded) - return; - - _settings.Add(setting.Name, setting); - } - - public void SaveSettings(WhWzSetting invokingSetting) - { - if (!_loaded) - return; - - var settingsToSave = new Dictionary(); - - foreach (var (name, setting) in _settings) - { - settingsToSave[name] = setting.Get(); - } - var jsonString = JsonSerializer.Serialize(settingsToSave, new JsonSerializerOptions { WriteIndented = true }); - FileHelper.WriteAllTextSafe(PathManager.WheelWizardConfigFilePath, jsonString); - } - - public void LoadSettings() - { - if (_loaded) - return; - - _loaded = true; - // even if it will now return early, that means its still done loading since then there is nothing to load - var jsonString = FileHelper.ReadAllTextSafe(PathManager.WheelWizardConfigFilePath); - if (jsonString == null) - return; - - try - { - var loadedSettings = JsonSerializer.Deserialize>(jsonString); - if (loadedSettings == null) - return; - - foreach (var kvp in loadedSettings) - { - if (!_settings.TryGetValue(kvp.Key, out var setting)) - continue; - - setting.SetFromJson(kvp.Value); - } - } - catch (JsonException e) - { - App.Services.GetRequiredService>().LogError(e, "Failed to deserialize the JSON config"); - } - } -} diff --git a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs index 8a327cd5..0809c289 100644 --- a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs +++ b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs @@ -1,10 +1,12 @@ using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; namespace WheelWizard.Services.WiiManagement; public static class WiiMoteSettings { + private static ISettingsManager Settings => SettingsRuntime.Current; + private const string WiimoteSection = "[Wiimote1]"; private const string SourceParameter = "Source"; @@ -12,7 +14,7 @@ public static class WiiMoteSettings public static void DisableVirtualWiiMote() => ModifyWiiMoteSource(0); - public static bool IsForceSettingsEnabled() => (bool)SettingsManager.FORCE_WIIMOTE.Get(); + public static bool IsForceSettingsEnabled() => Settings.Get(Settings.FORCE_WIIMOTE); private static string GetSavedWiiMoteLocation() { diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index 47f44225..88f5c6bc 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -6,10 +6,14 @@ using WheelWizard.Branding; using WheelWizard.CustomCharacters; using WheelWizard.CustomDistributions; +using WheelWizard.DolphinInstaller; using WheelWizard.GameBanana; using WheelWizard.GitHub; using WheelWizard.MiiImages; using WheelWizard.RrRooms; +using WheelWizard.Services.Launcher; +using WheelWizard.Services.LiveData; +using WheelWizard.Settings; using WheelWizard.Shared.Services; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement; @@ -25,6 +29,8 @@ public static class SetupExtensions public static void AddWheelWizardServices(this IServiceCollection services) { // Features + services.AddDolphinInstaller(); + services.AddSettings(); services.AddCustomCharacters(); services.AddAutoUpdating(); services.AddBranding(); @@ -48,5 +54,9 @@ public static void AddWheelWizardServices(this IServiceCollection services) // Dynamic API calls services.AddTransient(typeof(IApiCaller<>), typeof(ApiCaller<>)); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); } } diff --git a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs b/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs index 5682e6b1..06de1dcb 100644 --- a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs +++ b/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs @@ -1,7 +1,7 @@ using Avalonia; using Avalonia.Input; -using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Pages; using WheelWizard.WiiManagement; diff --git a/WheelWizard/Views/Layout.axaml.cs b/WheelWizard/Views/Layout.axaml.cs index 7a41c1fb..822be5b7 100644 --- a/WheelWizard/Views/Layout.axaml.cs +++ b/WheelWizard/Views/Layout.axaml.cs @@ -7,10 +7,10 @@ using Avalonia.Platform; using WheelWizard.Branding; using WheelWizard.Helpers; -using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.Views.Components; @@ -22,7 +22,7 @@ namespace WheelWizard.Views; -public partial class Layout : BaseWindow, IRepeatedTaskListener, ISettingListener +public partial class Layout : BaseWindow, IRepeatedTaskListener { protected override Control InteractionOverlay => DisabledDarkenEffect; protected override Control InteractionContent => CompleteGrid; @@ -39,6 +39,7 @@ public partial class Layout : BaseWindow, IRepeatedTaskListener, ISettingListene private const string TesterSecretPhrase = "WhenSonicInRR?"; private int _testerClickCount; private bool _testerPromptOpen; + private IDisposable? _settingsSignalSubscription; [Inject] private IBrandingSingletonService BrandingService { get; set; } = null!; @@ -46,15 +47,20 @@ public partial class Layout : BaseWindow, IRepeatedTaskListener, ISettingListene [Inject] private IGameLicenseSingletonService GameLicenseService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + + [Inject] + private ISettingsSignalBus SettingsSignalBus { get; set; } = null!; + public Layout() { Instance = this; InitializeComponent(); AddLayer(); - OnSettingChanged(SettingsManager.SAVED_WINDOW_SCALE); - SettingsManager.WINDOW_SCALE.Subscribe(this); - SettingsManager.TESTING_MODE_ENABLED.Subscribe(this); + OnSettingChanged(SettingsService.SAVED_WINDOW_SCALE); + _settingsSignalSubscription = SettingsSignalBus.Subscribe(OnSettingSignal); UpdateTestingButtonVisibility(); var completeString = Humanizer.ReplaceDynamic(Phrases.Text_MadeByString, "Patchzy", "WantToBeeMe"); @@ -91,10 +97,19 @@ protected override void OnLoaded(RoutedEventArgs e) NavigationManager.NavigateTo(); } - public void OnSettingChanged(Setting setting) + protected override void OnClosed(EventArgs e) + { + _settingsSignalSubscription?.Dispose(); + _settingsSignalSubscription = null; + base.OnClosed(e); + } + + private void OnSettingSignal(SettingChangedSignal signal) => OnSettingChanged(signal.Setting); + + private void OnSettingChanged(Setting setting) { // Note that this method will also be called whenever the setting changes - if (setting == SettingsManager.WINDOW_SCALE || setting == SettingsManager.SAVED_WINDOW_SCALE) + if (setting == SettingsService.WINDOW_SCALE || setting == SettingsService.SAVED_WINDOW_SCALE) { var scaleFactor = (double)setting.Get(); Height = WindowHeight * scaleFactor; @@ -107,7 +122,7 @@ public void OnSettingChanged(Setting setting) return; } - if (setting == SettingsManager.TESTING_MODE_ENABLED) + if (setting == SettingsService.TESTING_MODE_ENABLED) UpdateTestingButtonVisibility(); } @@ -235,7 +250,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve e.Handled = true; - if ((bool)SettingsManager.TESTING_MODE_ENABLED.Get()) + if (SettingsService.Get(SettingsService.TESTING_MODE_ENABLED)) return; if (_testerPromptOpen) @@ -261,7 +276,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve if (result == TesterSecretPhrase) { - SettingsManager.TESTING_MODE_ENABLED.Set(true); + SettingsService.Set(SettingsService.TESTING_MODE_ENABLED, true); ShowSnackbar("Testing mode enabled", ViewUtils.SnackbarType.Success); } else @@ -277,7 +292,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve private void UpdateTestingButtonVisibility() { - TestingButton.IsVisible = (bool)SettingsManager.TESTING_MODE_ENABLED.Get(); + TestingButton.IsVisible = SettingsService.Get(SettingsService.TESTING_MODE_ENABLED); } private void CloseButton_Click(object? sender, RoutedEventArgs e) => Close(); diff --git a/WheelWizard/Views/NavigationManager.cs b/WheelWizard/Views/NavigationManager.cs index 931b8d34..066e831c 100644 --- a/WheelWizard/Views/NavigationManager.cs +++ b/WheelWizard/Views/NavigationManager.cs @@ -1,11 +1,13 @@ using System.Globalization; using Avalonia.Controls; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; namespace WheelWizard.Views; public static class NavigationManager { + private static ISettingsManager Settings => SettingsRuntime.Current; + public static void NavigateTo(Type pageType, params object?[] args) { // TODO: Fix the language bug. for some reason when changing the language, it changes itself back to the language before @@ -13,11 +15,11 @@ public static void NavigateTo(Type pageType, params object?[] args) // still makes it so that the first page you enter after changing the language setting will always be the old language instead of the new one // when working on the translations again, this should be fixed. and in a solid way instead of this var itCurrentlyIs = CultureInfo.CurrentCulture.ToString(); - var itsSupposeToBe = (string)SettingsManager.WW_LANGUAGE.Get(); + var itsSupposeToBe = Settings.Get(Settings.WW_LANGUAGE); if (itCurrentlyIs != itsSupposeToBe) { - SettingsManager.WW_LANGUAGE.Set(itCurrentlyIs); - SettingsManager.WW_LANGUAGE.Set(itsSupposeToBe); + Settings.Set(Settings.WW_LANGUAGE, itCurrentlyIs); + Settings.Set(Settings.WW_LANGUAGE, itsSupposeToBe); } if (Activator.CreateInstance(pageType, args) is not UserControl instance) diff --git a/WheelWizard/Views/Pages/FriendsPage.axaml.cs b/WheelWizard/Views/Pages/FriendsPage.axaml.cs index ed8c4864..4a3afe4a 100644 --- a/WheelWizard/Views/Pages/FriendsPage.axaml.cs +++ b/WheelWizard/Views/Pages/FriendsPage.axaml.cs @@ -6,7 +6,7 @@ using WheelWizard.Resources.Languages; using WheelWizard.RrRooms; using WheelWizard.Services.LiveData; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Shared.Services; @@ -39,6 +39,9 @@ public partial class FriendsPage : UserControlBase, INotifyPropertyChanged, IRep [Inject] private IApiCaller ApiCaller { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public ObservableCollection FriendList { get => _friendlist; @@ -140,7 +143,7 @@ private void SortByDropdown_OnSelectionChanged(object? sender, SelectionChangedE private async void AddFriend_OnClick(object? sender, RoutedEventArgs e) { - var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get(); + var focusedUserIndex = SettingsService.Get(SettingsService.FOCUSED_USER); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); @@ -315,7 +318,7 @@ private void RemoveFriend_OnClick(object sender, RoutedEventArgs e) if (string.IsNullOrWhiteSpace(selectedPlayer.FriendCode)) return; - var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get(); + var focusedUserIndex = SettingsService.Get(SettingsService.FOCUSED_USER); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); diff --git a/WheelWizard/Views/Pages/HomePage.axaml.cs b/WheelWizard/Views/Pages/HomePage.axaml.cs index ca54024a..ccf8386c 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -9,7 +9,8 @@ using WheelWizard.Resources.Languages; using WheelWizard.Services.Launcher; using WheelWizard.Services.Launcher.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Components; using WheelWizard.Views.Pages.Settings; using Button = WheelWizard.Views.Components.Button; @@ -18,22 +19,27 @@ namespace WheelWizard.Views.Pages; public partial class HomePage : UserControlBase { - private static ILauncher currentLauncher => _launcherTypes[_launcherIndex]; - private static int _launcherIndex = 0; // Make sure this index never goes over the list index + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + + [Inject] + private RrLauncher RrLauncher { get; set; } = null!; + + [Inject] + private IRandomSystem RandomSystem { get; set; } = null!; + + private ILauncher CurrentLauncher => _launcherTypes[_launcherIndex]; + private int _launcherIndex; // Make sure this index never goes over the list index private WheelTrail[] _trails; // also used as a lock private WheelTrailState _currentTrailState = WheelTrailState.Static_None; - private static List _launcherTypes = - [ - new RrLauncher(), - //GoogleLauncher.Instance - ]; + private readonly List _launcherTypes = []; private WheelWizardStatus _status; - private MainButtonState currentButtonState => GetButtonState(_status); + private MainButtonState CurrentButtonState => GetButtonState(_status); - private static MainButtonState GetButtonState(WheelWizardStatus status) => + private MainButtonState GetButtonState(WheelWizardStatus status) => status switch { WheelWizardStatus.Loading => new(Common.State_Loading, Button.ButtonsVariantType.Default, "Spinner", null, false), @@ -68,18 +74,17 @@ private static MainButtonState GetButtonState(WheelWizardStatus status) => public HomePage() { InitializeComponent(); + _launcherTypes.Add(RrLauncher); PopulateGameModeDropdown(); UpdatePage(); _trails = [HomeTrail1, HomeTrail2, HomeTrail3, HomeTrail4, HomeTrail5]; - App.Services.GetService()?.Random.Shared.Shuffle(_trails); - // We have to do it like `App.Service.GetService`. We cant make use of `private IRandomSystem Random { get; set; } = null!;` here - // This is because this HomePage is always loaded first + RandomSystem.Random.Shared.Shuffle(_trails); } private void UpdatePage() { - GameTitle.Text = currentLauncher.GameTitle; + GameTitle.Text = CurrentLauncher.GameTitle; UpdateActionButton(); } @@ -89,29 +94,29 @@ private void DolphinButton_OnClick(object? sender, RoutedEventArgs e) DisableAllButtonsTemporarily(); } - private static void LaunchGame() => currentLauncher.Launch(); + private void LaunchGame() => _ = CurrentLauncher.Launch(); - private static void NavigateToSettings() => NavigationManager.NavigateTo(); + private void NavigateToSettings() => NavigationManager.NavigateTo(); - private static async void Download() + private async void Download() { ViewUtils.GetLayout().SetInteractable(false); - await currentLauncher.Install(); + await CurrentLauncher.Install(); ViewUtils.GetLayout().SetInteractable(true); NavigationManager.NavigateTo(); } - private static async void Update() + private async void Update() { ViewUtils.GetLayout().SetInteractable(false); - await currentLauncher.Update(); + await CurrentLauncher.Update(); ViewUtils.GetLayout().SetInteractable(true); NavigationManager.NavigateTo(); } private void PlayButton_Click(object? sender, RoutedEventArgs e) { - currentButtonState?.OnClick?.Invoke(); + CurrentButtonState?.OnClick?.Invoke(); PlayActivateAnimation(); UpdateActionButton(); DisableAllButtonsTemporarily(); @@ -141,9 +146,9 @@ private void PopulateGameModeDropdown() private async void UpdateActionButton() { _status = WheelWizardStatus.Loading; - SetButtonState(currentButtonState); - _status = await currentLauncher.GetCurrentStatus(); - SetButtonState(currentButtonState); + SetButtonState(CurrentButtonState); + _status = await CurrentLauncher.GetCurrentStatus(); + SetButtonState(CurrentButtonState); } private void DisableAllButtonsTemporarily() @@ -155,7 +160,7 @@ private void DisableAllButtonsTemporarily() { Dispatcher.UIThread.InvokeAsync(() => { - SetButtonState(currentButtonState); + SetButtonState(CurrentButtonState); return CompleteGrid.IsEnabled = true; }); }); @@ -168,7 +173,7 @@ private void SetButtonState(MainButtonState state) PlayButton.IsEnabled = state.OnClick != null; if (Application.Current != null && Application.Current.FindResource(state.IconName) is Geometry geometry) PlayButton.IconData = geometry; - DolphinButton.IsEnabled = state.SubButtonsEnabled && SettingsHelper.PathsSetupCorrectly(); + DolphinButton.IsEnabled = state.SubButtonsEnabled && SettingsService.PathsSetupCorrectly(); if (_status == WheelWizardStatus.Ready) PlayEntranceAnimation(); @@ -188,7 +193,7 @@ private async void PlayEntranceAnimation() // If the animations are disabled, it will never play the entrance animation // The entrance animation is also the only one that makes the wheels visible, meaning hat if this one does not play // all the other animations are all also impossible to play - if (!(bool)SettingsManager.ENABLE_ANIMATIONS.Get()) + if (!SettingsService.Get(SettingsService.ENABLE_ANIMATIONS)) return; var allowedToRun = WaitForWheelTrailState( @@ -219,7 +224,7 @@ private async void PlayEntranceAnimation() private async void PlayActivateAnimation() { - if (!(bool)SettingsManager.ENABLE_ANIMATIONS.Get()) + if (!SettingsService.Get(SettingsService.ENABLE_ANIMATIONS)) return; var allowedToRun = WaitForWheelTrailState( diff --git a/WheelWizard/Views/Pages/MiiListPage.axaml.cs b/WheelWizard/Views/Pages/MiiListPage.axaml.cs index a216fd91..1b81890d 100644 --- a/WheelWizard/Views/Pages/MiiListPage.axaml.cs +++ b/WheelWizard/Views/Pages/MiiListPage.axaml.cs @@ -9,7 +9,7 @@ using WheelWizard.Helpers; using WheelWizard.Resources.Languages; using WheelWizard.Services; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Components; @@ -38,6 +38,9 @@ public partial class MiiListPage : UserControlBase [Inject] private IRandomSystem Random { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public MiiListPage() { InitializeComponent(); @@ -46,7 +49,7 @@ public MiiListPage() var miiDbExists = MiiDbService.Exists(); if (!miiDbExists) { - if (SettingsHelper.PathsSetupCorrectly()) + if (SettingsService.PathsSetupCorrectly()) { var creationResult = MiiRepositoryService.ForceCreateDatabase(); if (creationResult.IsFailure) @@ -418,7 +421,7 @@ private async void CreateNewMii() if (!save) return; - var result = MiiDbService.AddToDatabase(window.Mii, (string)SettingsManager.MACADDRESS.Get()); + var result = MiiDbService.AddToDatabase(window.Mii, SettingsService.Get(SettingsService.MACADDRESS)); if (result.IsFailure) { ViewUtils.ShowSnackbar( @@ -434,7 +437,7 @@ private async void CreateNewMii() private void DuplicateMii(Mii[] miis) { //assuming the mac address is already set correctly - var macAddress = (string)SettingsManager.MACADDRESS.Get(); + var macAddress = SettingsService.Get(SettingsService.MACADDRESS); foreach (var mii in miis) { var result = MiiDbService.AddToDatabase(mii, macAddress); diff --git a/WheelWizard/Views/Pages/ModsPage.axaml.cs b/WheelWizard/Views/Pages/ModsPage.axaml.cs index 5301d174..beca7905 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml.cs +++ b/WheelWizard/Views/Pages/ModsPage.axaml.cs @@ -3,9 +3,10 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using WheelWizard.Models.Settings; +using WheelWizard.Models.Mods; using WheelWizard.Services; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Popups.Generic; using WheelWizard.Views.Popups.ModManagement; @@ -16,6 +17,9 @@ public record ModListItem(Mod Mod, bool IsLowest, bool IsHighest); public partial class ModsPage : UserControlBase, INotifyPropertyChanged { + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public ModManager ModManager => ModManager.Instance; public ObservableCollection Mods => @@ -179,15 +183,15 @@ private void ButtonDown_OnClick(object? sender, RoutedEventArgs e) private void ToggleModsPageView_OnClick(object? sender, RoutedEventArgs e) { - var current = (bool)SettingsManager.PREFERS_MODS_ROW_VIEW.Get(); - SettingsManager.PREFERS_MODS_ROW_VIEW.Set(!current); + var current = SettingsService.Get(SettingsService.PREFERS_MODS_ROW_VIEW); + SettingsService.Set(SettingsService.PREFERS_MODS_ROW_VIEW, !current); SetModsViewVariant(); } private void SetModsViewVariant() { Control[] elementsToSwapClasses = [ToggleButton, ModsListBox]; - var asRows = (bool)SettingsManager.PREFERS_MODS_ROW_VIEW.Get(); + var asRows = SettingsService.Get(SettingsService.PREFERS_MODS_ROW_VIEW); foreach (var elementToSwapClass in elementsToSwapClasses) { diff --git a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs index 5801f112..f5ec7339 100644 --- a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs +++ b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs @@ -6,7 +6,7 @@ using WheelWizard.Models.RRInfo; using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Utilities.Generators; using WheelWizard.Utilities.Mockers; @@ -27,6 +27,9 @@ public partial class RoomDetailsPage : UserControlBase, INotifyPropertyChanged, [Inject] private IMiiDbService MiiDbService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + private RrRoom _room = null!; public RrRoom Room @@ -132,7 +135,7 @@ private async void AddFriend_OnClick(object sender, RoutedEventArgs e) return; } - var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get(); + var focusedUserIndex = SettingsService.Get(SettingsService.FOCUSED_USER); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); diff --git a/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs b/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs index ab668408..43597e1a 100644 --- a/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Controls.Primitives; using WheelWizard.Branding; using WheelWizard.CustomDistributions; @@ -11,6 +10,9 @@ public partial class AppInfo : UserControlBase [Inject] private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + [Inject] + private IBrandingSingletonService BrandingSingletonService { get; set; } = null!; + public AppInfo() { InitializeComponent(); @@ -46,7 +48,7 @@ private void OpenLick_OnClick(object? sender, EventArgs e) protected override void OnInitialized() { - var branding = App.Services.GetRequiredService().Branding; + var branding = BrandingSingletonService.Branding; WhWzVersionText.Text = $"WhWz: v{branding.Version}"; base.OnInitialized(); } diff --git a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index 70ff8785..8eb6eea4 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -1,12 +1,7 @@ -using Avalonia.Controls; using Avalonia.Interactivity; using WheelWizard.CustomDistributions; -using WheelWizard.Helpers; -using WheelWizard.Models.Settings; -using WheelWizard.Resources.Languages; using WheelWizard.Services; -using WheelWizard.Services.Installation; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Generic; @@ -19,10 +14,13 @@ public partial class OtherSettings : UserControlBase [Inject] private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public OtherSettings() { InitializeComponent(); - _settingsAreDisabled = !SettingsHelper.PathsSetupCorrectly(); + _settingsAreDisabled = !SettingsService.PathsSetupCorrectly(); DisabledWarningText.IsVisible = _settingsAreDisabled; DolphinBorder.IsEnabled = !_settingsAreDisabled; @@ -38,8 +36,8 @@ public OtherSettings() private void LoadSettings() { // Only loads when the settings are not disabled (aka when the paths are set up correctly) - DisableForce.IsChecked = (bool)SettingsManager.FORCE_WIIMOTE.Get(); - LaunchWithDolphin.IsChecked = (bool)SettingsManager.LAUNCH_WITH_DOLPHIN.Get(); + DisableForce.IsChecked = SettingsService.Get(SettingsService.FORCE_WIIMOTE); + LaunchWithDolphin.IsChecked = SettingsService.Get(SettingsService.LAUNCH_WITH_DOLPHIN); OpenSaveFolderButton.IsEnabled = Directory.Exists(PathManager.SaveFolderPath); } @@ -50,12 +48,12 @@ private void ForceLoadSettings() private void ClickForceWiimote(object? sender, RoutedEventArgs e) { - SettingsManager.FORCE_WIIMOTE.Set(DisableForce.IsChecked == true); + SettingsService.Set(SettingsService.FORCE_WIIMOTE, DisableForce.IsChecked == true); } private void ClickLaunchWithDolphinWindow(object? sender, RoutedEventArgs e) { - SettingsManager.LAUNCH_WITH_DOLPHIN.Set(LaunchWithDolphin.IsChecked == true); + SettingsService.Set(SettingsService.LAUNCH_WITH_DOLPHIN, LaunchWithDolphin.IsChecked == true); } private async void Reinstall_RetroRewind(object sender, RoutedEventArgs e) diff --git a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs b/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs index b0093d93..2bde8008 100644 --- a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs @@ -1,9 +1,5 @@ using Avalonia.Controls; using Avalonia.Interactivity; -using WheelWizard.Branding; -using WheelWizard.CustomDistributions; -using WheelWizard.Services.Installation; -using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups; namespace WheelWizard.Views.Pages.Settings; diff --git a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs index 74b65613..37ed1df3 100644 --- a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs @@ -1,20 +1,23 @@ using Avalonia.Controls; using Avalonia.Interactivity; -using WheelWizard.Models.Settings; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; -using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Views.Pages.Settings; -public partial class VideoSettings : UserControl +public partial class VideoSettings : UserControlBase { private readonly bool _settingsAreDisabled; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public VideoSettings() { InitializeComponent(); - _settingsAreDisabled = !SettingsHelper.PathsSetupCorrectly(); + _settingsAreDisabled = !SettingsService.PathsSetupCorrectly(); DisabledWarningText.IsVisible = _settingsAreDisabled; VideoBorder.IsEnabled = !_settingsAreDisabled; @@ -38,12 +41,12 @@ public VideoSettings() private void LoadSettings() { // Load settings that are enabled for editing - VSyncButton.IsChecked = (bool)SettingsManager.VSYNC.Get(); - RecommendedButton.IsChecked = (bool)SettingsManager.RECOMMENDED_SETTINGS.Get(); - ShowFPSButton.IsChecked = (bool)SettingsManager.SHOW_FPS.Get(); - RemoveBlurButton.IsChecked = (bool)SettingsManager.REMOVE_BLUR.Get(); + VSyncButton.IsChecked = SettingsService.Get(SettingsService.VSYNC); + RecommendedButton.IsChecked = SettingsService.Get(SettingsService.RECOMMENDED_SETTINGS); + ShowFPSButton.IsChecked = SettingsService.Get(SettingsService.SHOW_FPS); + RemoveBlurButton.IsChecked = SettingsService.Get(SettingsService.REMOVE_BLUR); - var finalResolution = (int)SettingsManager.INTERNAL_RESOLUTION.Get(); + var finalResolution = SettingsService.Get(SettingsService.INTERNAL_RESOLUTION); foreach (RadioButton radioButton in ResolutionStackPanel.Children) { radioButton.IsChecked = (radioButton.Tag.ToString() == finalResolution.ToString()); @@ -58,7 +61,7 @@ private void ForceLoadSettings() RendererDropdown.Items.Add(renderer); } - var currentRenderer = (string)SettingsManager.GFX_BACKEND.Get(); + var currentRenderer = SettingsService.Get(SettingsService.GFX_BACKEND); var renderDisplayName = SettingValues.GFXRenderers.FirstOrDefault(x => x.Value == currentRenderer).Key; if (renderDisplayName != null) { @@ -70,28 +73,28 @@ private void UpdateResolution(object? sender, RoutedEventArgs e) { if (sender is RadioButton radioButton && radioButton.IsChecked == true) { - SettingsManager.INTERNAL_RESOLUTION.Set(int.Parse(radioButton.Tag.ToString()!)); + SettingsService.Set(SettingsService.INTERNAL_RESOLUTION, int.Parse(radioButton.Tag.ToString()!)); } } private void VSync_OnClick(object? sender, RoutedEventArgs e) { - SettingsManager.VSYNC.Set(VSyncButton.IsChecked == true); + SettingsService.Set(SettingsService.VSYNC, VSyncButton.IsChecked == true); } private void Recommended_OnClick(object? sender, RoutedEventArgs e) { - SettingsManager.RECOMMENDED_SETTINGS.Set(RecommendedButton.IsChecked == true); + SettingsService.Set(SettingsService.RECOMMENDED_SETTINGS, RecommendedButton.IsChecked == true); } private void ShowFPS_OnClick(object? sender, RoutedEventArgs e) { - SettingsManager.SHOW_FPS.Set(ShowFPSButton.IsChecked == true); + SettingsService.Set(SettingsService.SHOW_FPS, ShowFPSButton.IsChecked == true); } private void RemoveBlur_OnClick(object? sender, RoutedEventArgs e) { - SettingsManager.REMOVE_BLUR.Set(RemoveBlurButton.IsChecked == true); + SettingsService.Set(SettingsService.REMOVE_BLUR, RemoveBlurButton.IsChecked == true); } private void RendererDropdown_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) @@ -99,7 +102,7 @@ private void RendererDropdown_OnSelectionChanged(object? sender, SelectionChange var selectedDisplayName = RendererDropdown.SelectedItem?.ToString(); if (SettingValues.GFXRenderers.TryGetValue(selectedDisplayName, out var actualValue)) { - SettingsManager.GFX_BACKEND.Set(actualValue); + SettingsService.Set(SettingsService.GFX_BACKEND, actualValue); } else { diff --git a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs index fbf58c66..b3a87ad4 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs @@ -1,17 +1,17 @@ -using System.IO; using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; -using HarfBuzzSharp; using Serilog; +using WheelWizard.DolphinInstaller; using WheelWizard.Helpers; -using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; using WheelWizard.Services; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Popups.Generic; using Button = WheelWizard.Views.Components.Button; @@ -19,12 +19,21 @@ namespace WheelWizard.Views.Pages.Settings; -public partial class WhWzSettings : UserControl +public partial class WhWzSettings : UserControlBase { private readonly bool _pageLoaded; private bool _editingScale; private bool _isMovingAppData; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + + [Inject] + private IDolphinSettingManager DolphinSettingsService { get; set; } = null!; + + [Inject] + private ILinuxDolphinInstaller LinuxDolphinInstallerService { get; set; } = null!; + public WhWzSettings() { InitializeComponent(); @@ -49,7 +58,7 @@ private void LoadSettings() WhWzLanguageDropdown.Items.Add(lang()); } - var currentWhWzLanguage = (string)SettingsManager.WW_LANGUAGE.Get(); + var currentWhWzLanguage = (string)SettingsService.WW_LANGUAGE.Get(); var whWzLanguageDisplayName = SettingValues.WhWzLanguages[currentWhWzLanguage]; WhWzLanguageDropdown.SelectedItem = whWzLanguageDisplayName(); @@ -70,12 +79,12 @@ private void LoadSettings() WindowScaleDropdown.Items.Add(ScaleToString(scale)); } - var selectedItemText = ScaleToString((double)SettingsManager.WINDOW_SCALE.Get()); + var selectedItemText = ScaleToString((double)SettingsService.WINDOW_SCALE.Get()); if (!WindowScaleDropdown.Items.Contains(selectedItemText)) WindowScaleDropdown.Items.Add(selectedItemText); WindowScaleDropdown.SelectedItem = selectedItemText; - EnableAnimations.IsChecked = (bool)SettingsManager.ENABLE_ANIMATIONS.Get(); + EnableAnimations.IsChecked = (bool)SettingsService.ENABLE_ANIMATIONS.Get(); } private static string ScaleToString(double scale) @@ -138,11 +147,15 @@ private async void DolphinExeBrowse_OnClick(object sender, RoutedEventArgs e) TogglePathSettings(true); progressWindow.Show(); var progress = new Progress(progressWindow.UpdateProgress); - var success = await LinuxDolphinInstaller.InstallFlatpakDolphin(progress); + var installResult = await LinuxDolphinInstallerService.InstallFlatpakDolphin(progress); progressWindow.Close(); - if (!success) + if (installResult.IsFailure) { - await MessageTranslationHelper.AwaitMessageAsync(MessageTranslation.Error_FailedInstallDolphin); + await new MessageBoxWindow() + .SetMessageType(MessageBoxWindow.MessageType.Error) + .SetTitleText("Failed to install Dolphin") + .SetInfoText(installResult.Error.Message) + .ShowDialog(); return; } @@ -211,7 +224,7 @@ private async void DolphinExeBrowse_OnClick(object sender, RoutedEventArgs e) private bool IsFlatpakDolphinInstalled() { - return LinuxDolphinInstaller.IsDolphinInstalledInFlatpak(); + return LinuxDolphinInstallerService.IsDolphinInstalledInFlatpak(); } private async void GameLocationBrowse_OnClick(object sender, RoutedEventArgs e) @@ -248,7 +261,7 @@ private async void DolphinUserPathBrowse_OnClick(object sender, RoutedEventArgs await MessageTranslationHelper.AwaitMessageAsync(MessageTranslation.Warning_DolphinNotFound); } - var currentFolder = (string)SettingsManager.USER_FOLDER_PATH.Get(); + var currentFolder = (string)SettingsService.USER_FOLDER_PATH.Get(); var topLevel = TopLevel.GetTopLevel(this); // If a current folder exists and is valid, suggest it as the starting location if (!string.IsNullOrEmpty(currentFolder) && Directory.Exists(currentFolder)) @@ -280,16 +293,16 @@ private async void DolphinUserPathBrowse_OnClick(object sender, RoutedEventArgs private async void SaveButton_OnClick(object sender, RoutedEventArgs e) { - var oldPath1 = (string)SettingsManager.DOLPHIN_LOCATION.Get(); - var oldPath2 = (string)SettingsManager.GAME_LOCATION.Get(); - var oldPath3 = (string)SettingsManager.USER_FOLDER_PATH.Get(); + var oldPath1 = (string)SettingsService.DOLPHIN_LOCATION.Get(); + var oldPath2 = (string)SettingsService.GAME_LOCATION.Get(); + var oldPath3 = (string)SettingsService.USER_FOLDER_PATH.Get(); - var path1 = SettingsManager.DOLPHIN_LOCATION.Set(DolphinExeInput.Text); - var path2 = SettingsManager.GAME_LOCATION.Set(MarioKartInput.Text); - var path3 = SettingsManager.USER_FOLDER_PATH.Set(DolphinUserPathInput.Text.TrimEnd(Path.DirectorySeparatorChar)); + var path1 = SettingsService.DOLPHIN_LOCATION.Set(DolphinExeInput.Text); + var path2 = SettingsService.GAME_LOCATION.Set(MarioKartInput.Text); + var path3 = SettingsService.USER_FOLDER_PATH.Set(DolphinUserPathInput.Text.TrimEnd(Path.DirectorySeparatorChar)); // These 3 lines is only saving the settings TogglePathSettings(false); - if (!(SettingsHelper.PathsSetupCorrectly() && path1 && path2 && path3)) + if (!(SettingsService.PathsSetupCorrectly() && path1 && path2 && path3)) await MessageTranslationHelper.AwaitMessageAsync(MessageTranslation.Warning_InvalidPathSettings); else { @@ -297,7 +310,7 @@ private async void SaveButton_OnClick(object sender, RoutedEventArgs e) // This is not really the best approach, but it works for now if (oldPath1 + oldPath2 + oldPath3 != DolphinExeInput.Text + MarioKartInput.Text + DolphinUserPathInput.Text) - DolphinSettingManager.Instance.ReloadSettings(); + DolphinSettingsService.ReloadSettings(); } } @@ -326,7 +339,7 @@ private void TogglePathSettings(bool enable) { LocationBorder.BorderBrush = new SolidColorBrush(ViewUtils.Colors.Neutral900); } - else if (!SettingsHelper.PathsSetupCorrectly()) + else if (!SettingsService.PathsSetupCorrectly()) { LocationBorder.BorderBrush = new SolidColorBrush(ViewUtils.Colors.Warning400); LocationEditButton.Variant = Button.ButtonsVariantType.Warning; @@ -630,7 +643,7 @@ private async void WindowScaleDropdown_OnSelectionChanged(object sender, Selecti var selectedScale = WindowScaleDropdown.SelectedItem?.ToString() ?? "1"; var scale = double.Parse(selectedScale.Split(" ").Last().Replace("%", "")) / 100; - SettingsManager.WINDOW_SCALE.Set(scale); + SettingsService.WINDOW_SCALE.Set(scale); var seconds = 10; string ExtraScaleText() => @@ -657,11 +670,11 @@ string ExtraScaleText() => var yesNoAnswer = await yesNoWindow.AwaitAnswer(); if (yesNoAnswer) - SettingsManager.SAVED_WINDOW_SCALE.Set(SettingsManager.WINDOW_SCALE.Get()); + SettingsService.SAVED_WINDOW_SCALE.Set(SettingsService.WINDOW_SCALE.Get()); else { - SettingsManager.WINDOW_SCALE.Set(SettingsManager.SAVED_WINDOW_SCALE.Get()); - WindowScaleDropdown.SelectedItem = ScaleToString((double)SettingsManager.WINDOW_SCALE.Get()); + SettingsService.WINDOW_SCALE.Set(SettingsService.SAVED_WINDOW_SCALE.Get()); + WindowScaleDropdown.SelectedItem = ScaleToString((double)SettingsService.WINDOW_SCALE.Get()); } _editingScale = false; @@ -697,7 +710,7 @@ private async void WhWzLanguageDropdown_OnSelectionChanged(object? sender, Selec var selectedLanguage = WhWzLanguageDropdown.SelectedItem.ToString(); var key = SettingValues.WhWzLanguages.FirstOrDefault(x => x.Value() == selectedLanguage).Key; - var currentLanguage = (string)SettingsManager.WW_LANGUAGE.Get(); + var currentLanguage = (string)SettingsService.WW_LANGUAGE.Get(); if (key == null || key == currentLanguage) return; @@ -719,16 +732,16 @@ private async void WhWzLanguageDropdown_OnSelectionChanged(object? sender, Selec if (!yesNoWindow) { - var currentWhWzLanguage = (string)SettingsManager.WW_LANGUAGE.Get(); + var currentWhWzLanguage = (string)SettingsService.WW_LANGUAGE.Get(); var whWzLanguageDisplayName = SettingValues.WhWzLanguages[currentWhWzLanguage](); // gets the name of the current language back if the change was aborted WhWzLanguageDropdown.SelectedItem = whWzLanguageDisplayName; return; // We only want to change the setting if we really apply this change } - SettingsManager.WW_LANGUAGE.Set(key); + SettingsService.WW_LANGUAGE.Set(key); ViewUtils.RefreshWindow(); } private void EnableAnimations_OnClick(object sender, RoutedEventArgs e) => - SettingsManager.ENABLE_ANIMATIONS.Set(EnableAnimations.IsChecked == true); + SettingsService.ENABLE_ANIMATIONS.Set(EnableAnimations.IsChecked == true); } diff --git a/WheelWizard/Views/Pages/TestingPage.axaml.cs b/WheelWizard/Views/Pages/TestingPage.axaml.cs index 63bbb300..aada7ffc 100644 --- a/WheelWizard/Views/Pages/TestingPage.axaml.cs +++ b/WheelWizard/Views/Pages/TestingPage.axaml.cs @@ -2,8 +2,7 @@ using WheelWizard.CustomDistributions; using WheelWizard.Models.Enums; using WheelWizard.Services.Launcher; -using WheelWizard.Services.Launcher.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Generic; @@ -11,17 +10,21 @@ namespace WheelWizard.Views.Pages; public partial class TestingPage : UserControlBase { - private readonly ILauncher _launcher; private WheelWizardStatus _status = WheelWizardStatus.Loading; private bool _isBusy; [Inject] private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + + [Inject] + private RrBetaLauncher LauncherService { get; set; } = null!; + public TestingPage() { InitializeComponent(); - _launcher = new RrBetaLauncher(); UpdateStatusAsync(); } @@ -30,13 +33,13 @@ private async void UpdateStatusAsync() _status = WheelWizardStatus.Loading; UpdateUi(); - _status = await _launcher.GetCurrentStatus(); + _status = await LauncherService.GetCurrentStatus(); UpdateUi(); } private void UpdateUi() { - var pathsReady = SettingsHelper.PathsSetupCorrectly(); + var pathsReady = SettingsService.PathsSetupCorrectly(); var isInstalled = _status == WheelWizardStatus.Ready; InstallButton.IsEnabled = pathsReady && !_isBusy; @@ -67,7 +70,7 @@ private async void InstallButton_OnClick(object? sender, RoutedEventArgs e) _isBusy = true; UpdateUi(); - await _launcher.Install(); + await LauncherService.Install(); _isBusy = false; UpdateStatusAsync(); @@ -106,7 +109,7 @@ private async void LaunchButton_OnClick(object? sender, RoutedEventArgs e) _isBusy = true; UpdateUi(); - await _launcher.Launch(); + await LauncherService.Launch(); _isBusy = false; UpdateUi(); } diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs index fc94c937..ac6e1525 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs @@ -4,11 +4,11 @@ using Avalonia.Media; using WheelWizard.Helpers; using WheelWizard.Models.Enums; -using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Components; @@ -44,6 +44,9 @@ public partial class UserProfilePage : UserControlBase, INotifyPropertyChanged [Inject] private IMiiDbService MiiDbService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public Mii? CurrentMii { get => _currentMii; @@ -110,13 +113,13 @@ public int ActiveInfoSlideIndex } private int _currentUserIndex; - private static int FocussedUser => (int)SettingsManager.FOCUSSED_USER.Get(); + private int FocusedUser => SettingsService.Get(SettingsService.FOCUSED_USER); public UserProfilePage() { InitializeComponent(); ResetMiiTopBar(); - ViewMii(FocussedUser); + ViewMii(FocusedUser); PopulateRegions(); UpdatePage(); DataContext = this; @@ -128,7 +131,7 @@ public UserProfilePage() private void PopulateRegions() { var validRegions = RRRegionManager.GetValidRegions(); - var currentRegion = (MarioKartWiiEnums.Regions)SettingsManager.RR_REGION.Get(); + var currentRegion = SettingsService.Get(SettingsService.RR_REGION); foreach (var region in Enum.GetValues()) { if (region == MarioKartWiiEnums.Regions.None) @@ -198,7 +201,7 @@ private void ResetMiiTopBar() private void UpdatePage() { - PrimaryCheckBox.IsChecked = FocussedUser == _currentUserIndex; + PrimaryCheckBox.IsChecked = FocusedUser == _currentUserIndex; currentPlayer = GameLicenseService.GetUserData(_currentUserIndex); CurrentFriendCode = currentPlayer.FriendCode; @@ -238,10 +241,10 @@ private void ViewMii(int? mii = null) private void SetUserAsPrimary() { - if (FocussedUser == _currentUserIndex) + if (FocusedUser == _currentUserIndex) return; - SettingsManager.FOCUSSED_USER.Set(_currentUserIndex); + SettingsService.Set(SettingsService.FOCUSED_USER, _currentUserIndex); PrimaryCheckBox.IsChecked = true; // Even though it's true when this method is called, we still set it to true, @@ -257,7 +260,7 @@ private void RegionDropdown_SelectionChanged(object sender, SelectionChangedEven if (RegionDropdown.SelectedItem is not ComboBoxItem { Tag: MarioKartWiiEnums.Regions region }) return; - SettingsManager.RR_REGION.Set(region); + SettingsService.Set(SettingsService.RR_REGION, region); ResetMiiTopBar(); var loadResult = GameLicenseService.LoadLicense(); if (loadResult.IsFailure) diff --git a/WheelWizard/Views/Popups/Base/PopupWindow.axaml.cs b/WheelWizard/Views/Popups/Base/PopupWindow.axaml.cs index 8d7dce31..bed94f8d 100644 --- a/WheelWizard/Views/Popups/Base/PopupWindow.axaml.cs +++ b/WheelWizard/Views/Popups/Base/PopupWindow.axaml.cs @@ -4,12 +4,16 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; namespace WheelWizard.Views.Popups.Base; public partial class PopupWindow : BaseWindow, INotifyPropertyChanged { + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + protected override Control InteractionOverlay => DisabledDarkenEffect; protected override Control InteractionContent => CompleteGrid; @@ -126,7 +130,7 @@ protected override void OnResized(WindowResizedEventArgs e) public void SetWindowSize(Size size) { - var scaleFactor = (double)SettingsManager.WINDOW_SCALE.Get(); + var scaleFactor = SettingsService.Get(SettingsService.WINDOW_SCALE); Width = size.Width * scaleFactor; Height = size.Height * scaleFactor; CompleteGrid.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor);