From 42f81be865b3359f98b92cdf6b8bd22c1bd530a0 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:46:35 +0100 Subject: [PATCH 01/15] first rewrite --- .../CustomDistributionSingletonService.cs | 12 +- .../CustomDistributions/RetroRewind.cs | 13 +- .../CustomDistributions/RetroRewindBeta.cs | 8 +- .../Settings/DolphinSettingManager.cs | 9 +- .../Features/Settings/ISettingsServices.cs | 78 +++++ .../Settings/LinuxDolphinInstaller.cs | 7 +- .../Settings/ModConfigManager.cs | 0 .../Features/Settings/SettingsExtensions.cs | 14 + .../Features/Settings/SettingsManager.cs | 320 ++++++++++++++++++ .../Settings/SettingsStartupInitializer.cs | 9 + WheelWizard/Features/Settings/TypedSetting.cs | 26 ++ .../Settings/WhWzSettingManager.cs | 17 +- .../GameLicense/GameLicenseService.cs | 19 +- .../MiiManagement/MiiExtensions.cs | 8 +- WheelWizard/Models/Settings/DolphinSetting.cs | 24 +- WheelWizard/Models/Settings/WhWzSetting.cs | 22 +- WheelWizard/Program.cs | 4 +- .../Launcher/Helpers/DolphinLaunchHelper.cs | 8 +- .../Services/Launcher/RrBetaLauncher.cs | 25 +- WheelWizard/Services/Launcher/RrLauncher.cs | 25 +- WheelWizard/Services/PathManager.cs | 20 +- .../Services/Settings/SettingsHelper.cs | 39 --- .../Services/Settings/SettingsManager.cs | 196 ----------- .../Services/WiiManagement/WiiMoteSettings.cs | 10 +- WheelWizard/SetupExtensions.cs | 5 + WheelWizard/Views/Layout.axaml.cs | 21 +- WheelWizard/Views/NavigationManager.cs | 11 +- WheelWizard/Views/Pages/FriendsPage.axaml.cs | 9 +- WheelWizard/Views/Pages/HomePage.axaml.cs | 12 +- WheelWizard/Views/Pages/MiiListPage.axaml.cs | 11 +- WheelWizard/Views/Pages/ModsPage.axaml.cs | 12 +- .../Views/Pages/RoomDetailsPage.axaml.cs | 7 +- .../Pages/Settings/OtherSettings.axaml.cs | 15 +- .../Pages/Settings/VideoSettings.axaml.cs | 35 +- .../Pages/Settings/WhWzSettings.axaml.cs | 54 +-- WheelWizard/Views/Pages/TestingPage.axaml.cs | 9 +- .../Views/Pages/UserProfilePage.axaml.cs | 13 +- .../Views/Popups/Base/PopupWindow.axaml.cs | 8 +- 38 files changed, 729 insertions(+), 406 deletions(-) rename WheelWizard/{Services => Features}/Settings/DolphinSettingManager.cs (96%) create mode 100644 WheelWizard/Features/Settings/ISettingsServices.cs rename WheelWizard/{Services => Features}/Settings/LinuxDolphinInstaller.cs (97%) rename WheelWizard/{Services => Features}/Settings/ModConfigManager.cs (100%) create mode 100644 WheelWizard/Features/Settings/SettingsExtensions.cs create mode 100644 WheelWizard/Features/Settings/SettingsManager.cs create mode 100644 WheelWizard/Features/Settings/SettingsStartupInitializer.cs create mode 100644 WheelWizard/Features/Settings/TypedSetting.cs rename WheelWizard/{Services => Features}/Settings/WhWzSettingManager.cs (77%) delete mode 100644 WheelWizard/Services/Settings/SettingsHelper.cs delete mode 100644 WheelWizard/Services/Settings/SettingsManager.cs 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/Services/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs similarity index 96% rename from WheelWizard/Services/Settings/DolphinSettingManager.cs rename to WheelWizard/Features/Settings/DolphinSettingManager.cs index 9e11bffd..3f137799 100644 --- a/WheelWizard/Services/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -1,19 +1,16 @@ using WheelWizard.Helpers; using WheelWizard.Models.Settings; +using WheelWizard.Services; -namespace WheelWizard.Services.Settings; +namespace WheelWizard.Settings; -public class DolphinSettingManager +public class DolphinSettingManager : IDolphinSettingManager { 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) diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs new file mode 100644 index 00000000..004c46f7 --- /dev/null +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -0,0 +1,78 @@ +using WheelWizard.Models.Settings; + +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 ITypedSetting +{ + string Name { get; } + T Get(); + bool Set(T value, bool skipSave = false); + bool IsValid(); + Setting RawSetting { get; } +} + +public interface ISettingsManager +{ + 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 FOCUSSED_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; } + + ITypedSetting UserFolderPath { get; } + ITypedSetting DolphinLocation { get; } + ITypedSetting GameLocation { get; } + ITypedSetting ForceWiimote { get; } + ITypedSetting LaunchWithDolphin { get; } + ITypedSetting PrefersModsRowView { get; } + ITypedSetting FocussedUser { get; } + ITypedSetting EnableAnimations { get; } + ITypedSetting TestingModeEnabled { get; } + ITypedSetting SavedWindowScale { get; } + ITypedSetting RemoveBlur { get; } + ITypedSetting WwLanguage { get; } + ITypedSetting MacAddress { get; } + + T Get(Setting setting); + bool Set(Setting setting, T value, bool skipSave = false); + bool PathsSetupCorrectly(); + void LoadSettings(); +} + +public interface ISettingsStartupInitializer +{ + void Initialize(); +} diff --git a/WheelWizard/Services/Settings/LinuxDolphinInstaller.cs b/WheelWizard/Features/Settings/LinuxDolphinInstaller.cs similarity index 97% rename from WheelWizard/Services/Settings/LinuxDolphinInstaller.cs rename to WheelWizard/Features/Settings/LinuxDolphinInstaller.cs index e8e02aad..a6cccd7f 100644 --- a/WheelWizard/Services/Settings/LinuxDolphinInstaller.cs +++ b/WheelWizard/Features/Settings/LinuxDolphinInstaller.cs @@ -4,7 +4,7 @@ using WheelWizard.Helpers; using WheelWizard.Views.Popups.Generic; -namespace WheelWizard.Services.Settings; +namespace WheelWizard.Settings; public static class LinuxDolphinInstaller { @@ -23,6 +23,9 @@ public static bool IsDolphinInstalledInFlatpak() }; using var process = Process.Start(processInfo); + if (process == null) + return false; + process.WaitForExit(); return process.ExitCode == 0; } @@ -46,6 +49,8 @@ private static async Task RunProcessWithProgressAsync(string fileName, stri }; using var process = Process.Start(processInfo); + if (process == null) + return -1; // Listen for output data to parse progress. process.OutputDataReceived += (sender, e) => diff --git a/WheelWizard/Services/Settings/ModConfigManager.cs b/WheelWizard/Features/Settings/ModConfigManager.cs similarity index 100% rename from WheelWizard/Services/Settings/ModConfigManager.cs rename to WheelWizard/Features/Settings/ModConfigManager.cs diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs new file mode 100644 index 00000000..7b6440de --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -0,0 +1,14 @@ +namespace WheelWizard.Settings; + +public static class SettingsExtensions +{ + public static IServiceCollection AddSettings(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs new file mode 100644 index 00000000..24a937c8 --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -0,0 +1,320 @@ +using System.Globalization; +using System.Runtime.InteropServices; +using WheelWizard.Helpers; +using WheelWizard.Models.Enums; +using WheelWizard.Models.Settings; +using WheelWizard.Services; + +namespace WheelWizard.Settings; + +public class SettingsManager : ISettingsManager, ISettingListener +{ + private readonly IWhWzSettingManager _whWzSettingManager; + private readonly IDolphinSettingManager _dolphinSettingManager; + + private readonly Setting _dolphinCompilationMode; + private readonly Setting _dolphinCompileShadersAtStart; + private readonly Setting _dolphinSsaa; + private readonly Setting _dolphinMsaa; + + private bool _hasInitializedLanguageSync; + private double _internalScale = -1.0; + + public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager) + { + _whWzSettingManager = whWzSettingManager; + _dolphinSettingManager = dolphinSettingManager; + + DOLPHIN_LOCATION = RegisterWhWz( + CreateWhWzSetting(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); + }) + ); + + USER_FOLDER_PATH = RegisterWhWz( + CreateWhWzSetting(typeof(string), "UserFolderPath", "") + .SetValidation(value => + { + var userFolderPath = value as string ?? string.Empty; + if (!FileHelper.DirectoryExists(userFolderPath)) + return false; + + string 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 + // (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 + 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() && FileHelper.DirectoryExists(PathManager.LinuxDolphinLegacyFolderPath)) + return false; + } + + return true; + }) + ); + + GAME_LOCATION = RegisterWhWz( + CreateWhWzSetting(typeof(string), "GameLocation", "") + .SetValidation(value => FileHelper.FileExists(value as string ?? string.Empty)) + ); + FORCE_WIIMOTE = RegisterWhWz(CreateWhWzSetting(typeof(bool), "ForceWiimote", false)); + LAUNCH_WITH_DOLPHIN = RegisterWhWz(CreateWhWzSetting(typeof(bool), "LaunchWithDolphin", false)); + PREFERS_MODS_ROW_VIEW = RegisterWhWz(CreateWhWzSetting(typeof(bool), "PrefersModsRowView", true)); + FOCUSSED_USER = RegisterWhWz( + CreateWhWzSetting(typeof(int), "FavoriteUser", 0).SetValidation(value => (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4) + ); + + ENABLE_ANIMATIONS = RegisterWhWz(CreateWhWzSetting(typeof(bool), "EnableAnimations", true)); + TESTING_MODE_ENABLED = RegisterWhWz(CreateWhWzSetting(typeof(bool), "TestingModeEnabled", false)); + SAVED_WINDOW_SCALE = RegisterWhWz( + CreateWhWzSetting(typeof(double), "WindowScale", 1.0) + .SetValidation(value => (double)(value ?? -1) >= 0.5 && (double)(value ?? -1) <= 2.0) + ); + REMOVE_BLUR = RegisterWhWz(CreateWhWzSetting(typeof(bool), "REMOVE_BLUR", true)); + RR_REGION = RegisterWhWz(CreateWhWzSetting(typeof(MarioKartWiiEnums.Regions), "RR_Region", MarioKartWiiEnums.Regions.None)); + WW_LANGUAGE = RegisterWhWz( + CreateWhWzSetting(typeof(string), "WW_Language", "en") + .SetValidation(value => SettingValues.WhWzLanguages.ContainsKey((string)value!)) + ); + + NAND_ROOT_PATH = RegisterDolphin( + CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "") + .SetValidation(value => Directory.Exists(value as string ?? string.Empty)) + ); + + LOAD_PATH = RegisterDolphin( + CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "LoadPath"), "") + .SetValidation(value => Directory.Exists(value as string ?? string.Empty)) + ); + + VSYNC = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Hardware", "VSync"), false)); + INTERNAL_RESOLUTION = RegisterDolphin( + CreateDolphinSetting(typeof(int), ("GFX.ini", "Settings", "InternalResolution"), 1) + .SetValidation(value => (int)(value ?? -1) >= 0) + ); + SHOW_FPS = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "ShowFPS"), false)); + GFX_BACKEND = RegisterDolphin( + CreateDolphinSetting(typeof(string), ("Dolphin.ini", "Core", "GFXBackend"), SettingValues.GFXRenderers.Values.First()) + ); + + // recommended settings + _dolphinCompilationMode = RegisterDolphin( + CreateDolphinSetting( + typeof(DolphinShaderCompilationMode), + ("GFX.ini", "Settings", "ShaderCompilationMode"), + DolphinShaderCompilationMode.Default + ) + ); + _dolphinCompileShadersAtStart = RegisterDolphin( + CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "WaitForShadersBeforeStarting"), false) + ); + _dolphinSsaa = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "SSAA"), false)); + _dolphinMsaa = RegisterDolphin( + CreateDolphinSetting(typeof(string), ("GFX.ini", "Settings", "MSAA"), "0x00000001") + .SetValidation(value => (value?.ToString() ?? "") is "0x00000001" or "0x00000002" or "0x00000004" or "0x00000008") + ); + + // Readonly settings + MACADDRESS = RegisterDolphin(CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "WirelessMac"), "02:01:02:03:04:05")); + + 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); + + UserFolderPath = new TypedSetting(USER_FOLDER_PATH); + DolphinLocation = new TypedSetting(DOLPHIN_LOCATION); + GameLocation = new TypedSetting(GAME_LOCATION); + ForceWiimote = new TypedSetting(FORCE_WIIMOTE); + LaunchWithDolphin = new TypedSetting(LAUNCH_WITH_DOLPHIN); + PrefersModsRowView = new TypedSetting(PREFERS_MODS_ROW_VIEW); + FocussedUser = new TypedSetting(FOCUSSED_USER); + EnableAnimations = new TypedSetting(ENABLE_ANIMATIONS); + TestingModeEnabled = new TypedSetting(TESTING_MODE_ENABLED); + SavedWindowScale = new TypedSetting(SAVED_WINDOW_SCALE); + RemoveBlur = new TypedSetting(REMOVE_BLUR); + WwLanguage = new TypedSetting(WW_LANGUAGE); + MacAddress = new TypedSetting(MACADDRESS); + } + + 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 FOCUSSED_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; } + + public ITypedSetting UserFolderPath { get; } + public ITypedSetting DolphinLocation { get; } + public ITypedSetting GameLocation { get; } + public ITypedSetting ForceWiimote { get; } + public ITypedSetting LaunchWithDolphin { get; } + public ITypedSetting PrefersModsRowView { get; } + public ITypedSetting FocussedUser { get; } + public ITypedSetting EnableAnimations { get; } + public ITypedSetting TestingModeEnabled { get; } + public ITypedSetting SavedWindowScale { get; } + public ITypedSetting RemoveBlur { get; } + public ITypedSetting WwLanguage { get; } + public ITypedSetting MacAddress { get; } + + 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() + { + return USER_FOLDER_PATH.IsValid() && DOLPHIN_LOCATION.IsValid() && GAME_LOCATION.IsValid(); + } + + public void LoadSettings() + { + _whWzSettingManager.LoadSettings(); + _dolphinSettingManager.LoadSettings(); + + if (_hasInitializedLanguageSync) + return; + + WW_LANGUAGE.Subscribe(this); + OnWheelWizardLanguageChange(); + _hasInitializedLanguageSync = true; + } + + public void OnSettingChanged(Setting setting) + { + if (setting == WW_LANGUAGE) + OnWheelWizardLanguageChange(); + } + + private void OnWheelWizardLanguageChange() + { + var newCulture = new CultureInfo(WwLanguage.Get()); + CultureInfo.CurrentCulture = newCulture; + CultureInfo.CurrentUICulture = newCulture; + } + + private WhWzSetting CreateWhWzSetting(Type valueType, string name, object defaultValue) + { + return new(valueType, name, defaultValue, _whWzSettingManager.SaveSettings); + } + + private DolphinSetting CreateDolphinSetting(Type valueType, (string, string, string) location, object defaultValue) + { + return new(valueType, location, defaultValue, _dolphinSettingManager.SaveSettings); + } + + private Setting RegisterWhWz(WhWzSetting setting) + { + _whWzSettingManager.RegisterSetting(setting); + return setting; + } + + private Setting RegisterDolphin(DolphinSetting setting) + { + _dolphinSettingManager.RegisterSetting(setting); + return setting; + } +} diff --git a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs new file mode 100644 index 00000000..d2cb8051 --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs @@ -0,0 +1,9 @@ +namespace WheelWizard.Settings; + +public sealed class SettingsStartupInitializer(ISettingsManager settingsManager) : ISettingsStartupInitializer +{ + public void Initialize() + { + settingsManager.LoadSettings(); + } +} diff --git a/WheelWizard/Features/Settings/TypedSetting.cs b/WheelWizard/Features/Settings/TypedSetting.cs new file mode 100644 index 00000000..b5e98bb8 --- /dev/null +++ b/WheelWizard/Features/Settings/TypedSetting.cs @@ -0,0 +1,26 @@ +using WheelWizard.Models.Settings; + +namespace WheelWizard.Settings; + +public sealed class TypedSetting : ITypedSetting +{ + public TypedSetting(Setting rawSetting) + { + RawSetting = rawSetting; + } + + public string Name => RawSetting.Name; + public Setting RawSetting { get; } + + public T Get() => (T)RawSetting.Get(); + + public bool IsValid() => RawSetting.IsValid(); + + public bool Set(T value, bool skipSave = false) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return RawSetting.Set(value, skipSave); + } +} diff --git a/WheelWizard/Services/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs similarity index 77% rename from WheelWizard/Services/Settings/WhWzSettingManager.cs rename to WheelWizard/Features/Settings/WhWzSettingManager.cs index df6fa3d3..bc7474e7 100644 --- a/WheelWizard/Services/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -2,27 +2,21 @@ 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; +using WheelWizard.Services; -namespace WheelWizard.Services.Settings; +namespace WheelWizard.Settings; -public class WhWzSettingManager +public class WhWzSettingManager(ILogger logger) : IWhWzSettingManager { 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); + _settings[setting.Name] = setting; } public void SaveSettings(WhWzSetting invokingSetting) @@ -36,6 +30,7 @@ public void SaveSettings(WhWzSetting invokingSetting) { settingsToSave[name] = setting.Get(); } + var jsonString = JsonSerializer.Serialize(settingsToSave, new JsonSerializerOptions { WriteIndented = true }); FileHelper.WriteAllTextSafe(PathManager.WheelWizardConfigFilePath, jsonString); } @@ -67,7 +62,7 @@ public void LoadSettings() } catch (JsonException e) { - App.Services.GetRequiredService>().LogError(e, "Failed to deserialize the JSON config"); + 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..c7e62204 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -7,7 +7,7 @@ using WheelWizard.Services; using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; 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.FocussedUser.Get()]; - public List ActiveCurrentFriends => Licenses.Users[(int)SettingsManager.FOCUSSED_USER.Get()].Friends; + public List ActiveCurrentFriends => Licenses.Users[_settingsManager.FocussedUser.Get()].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..63156681 100644 --- a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs +++ b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs @@ -1,10 +1,14 @@ -using WheelWizard.Services.Settings; +using Microsoft.Extensions.DependencyInjection; +using WheelWizard.Settings; +using WheelWizard.Views; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; namespace WheelWizard.WiiManagement.MiiManagement; public static class MiiExtensions { + private static ISettingsManager Settings => App.Services.GetRequiredService(); + 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 +74,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.MacAddress.Get(); var macParts = macAddressString.Split(':'); var macBytes = new byte[6]; for (var i = 0; i < 6; i++) diff --git a/WheelWizard/Models/Settings/DolphinSetting.cs b/WheelWizard/Models/Settings/DolphinSetting.cs index 84a5b0ad..0db1b17f 100644 --- a/WheelWizard/Models/Settings/DolphinSetting.cs +++ b/WheelWizard/Models/Settings/DolphinSetting.cs @@ -1,15 +1,19 @@ -using WheelWizard.Services.Settings; - namespace WheelWizard.Models.Settings; 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; 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/WhWzSetting.cs b/WheelWizard/Models/Settings/WhWzSetting.cs index 7a76c884..f47eeb61 100644 --- a/WheelWizard/Models/Settings/WhWzSetting.cs +++ b/WheelWizard/Models/Settings/WhWzSetting.cs @@ -1,14 +1,18 @@ using System.Text.Json; -using WheelWizard.Services.Settings; namespace WheelWizard.Models.Settings; 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; } 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/Program.cs b/WheelWizard/Program.cs index 6eaa931c..e5f02eb6 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; @@ -108,7 +108,7 @@ private static void SetupWorkingDirectory() private static void Setup() { - SettingsManager.Instance.LoadSettings(); + App.Services.GetRequiredService().Initialize(); UrlProtocolManager.SetWhWzScheme(); } } diff --git a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs index bb4989fb..4bd6a347 100644 --- a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs +++ b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs @@ -1,14 +1,18 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Services.Launcher.Helpers; public static class DolphinLaunchHelper { + private static ISettingsManager Settings => App.Services.GetRequiredService(); + public static void KillDolphin() //dont tell PETA { var dolphinLocation = PathManager.DolphinFilePath; @@ -130,7 +134,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.DolphinLocation.Get(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Windows builds diff --git a/WheelWizard/Services/Launcher/RrBetaLauncher.cs b/WheelWizard/Services/Launcher/RrBetaLauncher.cs index 1bcba6b9..7771c68e 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.LaunchWithDolphin.Get() ? "" : "-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..81c4ad84 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.LaunchWithDolphin.Get() ? "" : "-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/PathManager.cs b/WheelWizard/Services/PathManager.cs index 4417cd1f..d2198537 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Views; #if WINDOWS using Microsoft.Win32; #endif @@ -9,6 +11,8 @@ namespace WheelWizard.Services; public static class PathManager { + private static ISettingsManager Settings => App.Services.GetRequiredService(); + // IMPORTANT: To keep things consistent all paths should be Attrib expressions, // and either end with `FilePath` or `FolderPath` @@ -34,9 +38,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.GameLocation.Get(); + public static string DolphinFilePath => Settings.DolphinLocation.Get(); + public static string UserFolderPath => Settings.UserFolderPath.Get(); private static string AppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); private static string LocalAppDataFolder => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -603,9 +607,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 +641,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/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/WiiManagement/WiiMoteSettings.cs b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs index 8a327cd5..90da4b64 100644 --- a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs +++ b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs @@ -1,10 +1,14 @@ -using WheelWizard.Helpers; -using WheelWizard.Services.Settings; +using Microsoft.Extensions.DependencyInjection; +using WheelWizard.Helpers; +using WheelWizard.Settings; +using WheelWizard.Views; namespace WheelWizard.Services.WiiManagement; public static class WiiMoteSettings { + private static ISettingsManager Settings => App.Services.GetRequiredService(); + private const string WiimoteSection = "[Wiimote1]"; private const string SourceParameter = "Source"; @@ -12,7 +16,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.ForceWiimote.Get(); private static string GetSavedWiiMoteLocation() { diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index 47f44225..cce19b50 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -10,6 +10,8 @@ using WheelWizard.GitHub; using WheelWizard.MiiImages; using WheelWizard.RrRooms; +using WheelWizard.Services.Launcher; +using WheelWizard.Settings; using WheelWizard.Shared.Services; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement; @@ -25,6 +27,7 @@ public static class SetupExtensions public static void AddWheelWizardServices(this IServiceCollection services) { // Features + services.AddSettings(); services.AddCustomCharacters(); services.AddAutoUpdating(); services.AddBranding(); @@ -48,5 +51,7 @@ public static void AddWheelWizardServices(this IServiceCollection services) // Dynamic API calls services.AddTransient(typeof(IApiCaller<>), typeof(ApiCaller<>)); + services.AddTransient(); + services.AddTransient(); } } diff --git a/WheelWizard/Views/Layout.axaml.cs b/WheelWizard/Views/Layout.axaml.cs index 7a41c1fb..01ce965d 100644 --- a/WheelWizard/Views/Layout.axaml.cs +++ b/WheelWizard/Views/Layout.axaml.cs @@ -10,7 +10,7 @@ using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.Views.Components; @@ -46,15 +46,18 @@ public partial class Layout : BaseWindow, IRepeatedTaskListener, ISettingListene [Inject] private IGameLicenseSingletonService GameLicenseService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { 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); + SettingsService.WINDOW_SCALE.Subscribe(this); + SettingsService.TESTING_MODE_ENABLED.Subscribe(this); UpdateTestingButtonVisibility(); var completeString = Humanizer.ReplaceDynamic(Phrases.Text_MadeByString, "Patchzy", "WantToBeeMe"); @@ -94,7 +97,7 @@ protected override void OnLoaded(RoutedEventArgs e) public 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 +110,7 @@ public void OnSettingChanged(Setting setting) return; } - if (setting == SettingsManager.TESTING_MODE_ENABLED) + if (setting == SettingsService.TESTING_MODE_ENABLED) UpdateTestingButtonVisibility(); } @@ -235,7 +238,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve e.Handled = true; - if ((bool)SettingsManager.TESTING_MODE_ENABLED.Get()) + if (SettingsService.TestingModeEnabled.Get()) return; if (_testerPromptOpen) @@ -261,7 +264,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 +280,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve private void UpdateTestingButtonVisibility() { - TestingButton.IsVisible = (bool)SettingsManager.TESTING_MODE_ENABLED.Get(); + TestingButton.IsVisible = SettingsService.TestingModeEnabled.Get(); } private void CloseButton_Click(object? sender, RoutedEventArgs e) => Close(); diff --git a/WheelWizard/Views/NavigationManager.cs b/WheelWizard/Views/NavigationManager.cs index 931b8d34..d270aef6 100644 --- a/WheelWizard/Views/NavigationManager.cs +++ b/WheelWizard/Views/NavigationManager.cs @@ -1,11 +1,14 @@ using System.Globalization; using Avalonia.Controls; -using WheelWizard.Services.Settings; +using Microsoft.Extensions.DependencyInjection; +using WheelWizard.Settings; namespace WheelWizard.Views; public static class NavigationManager { + private static ISettingsManager Settings => App.Services.GetRequiredService(); + 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 +16,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.WwLanguage.Get(); 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..ad064955 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.FocussedUser.Get(); 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.FocussedUser.Get(); 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..e06a3135 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -4,12 +4,13 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; +using Microsoft.Extensions.DependencyInjection; using Testably.Abstractions; using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services.Launcher; using WheelWizard.Services.Launcher.Helpers; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; using WheelWizard.Views.Components; using WheelWizard.Views.Pages.Settings; using Button = WheelWizard.Views.Components.Button; @@ -18,6 +19,7 @@ namespace WheelWizard.Views.Pages; public partial class HomePage : UserControlBase { + private static ISettingsManager SettingsService => App.Services.GetRequiredService(); private static ILauncher currentLauncher => _launcherTypes[_launcherIndex]; private static int _launcherIndex = 0; // Make sure this index never goes over the list index @@ -26,7 +28,7 @@ public partial class HomePage : UserControlBase private static List _launcherTypes = [ - new RrLauncher(), + App.Services.GetRequiredService(), //GoogleLauncher.Instance ]; @@ -168,7 +170,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 +190,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.EnableAnimations.Get()) return; var allowedToRun = WaitForWheelTrailState( @@ -219,7 +221,7 @@ private async void PlayEntranceAnimation() private async void PlayActivateAnimation() { - if (!(bool)SettingsManager.ENABLE_ANIMATIONS.Get()) + if (!SettingsService.EnableAnimations.Get()) return; var allowedToRun = WaitForWheelTrailState( diff --git a/WheelWizard/Views/Pages/MiiListPage.axaml.cs b/WheelWizard/Views/Pages/MiiListPage.axaml.cs index a216fd91..b3c84cdf 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.MacAddress.Get()); 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.MacAddress.Get(); 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..0f1ffd59 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml.cs +++ b/WheelWizard/Views/Pages/ModsPage.axaml.cs @@ -5,7 +5,8 @@ using Avalonia.Interactivity; using WheelWizard.Models.Settings; 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.PrefersModsRowView.Get(); + 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.PrefersModsRowView.Get(); foreach (var elementToSwapClass in elementsToSwapClasses) { diff --git a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs index 5801f112..0ffd67eb 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.FocussedUser.Get(); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); diff --git a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index 70ff8785..4f058dea 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -6,7 +6,7 @@ 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 +19,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 +41,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.ForceWiimote.Get(); + LaunchWithDolphin.IsChecked = SettingsService.LaunchWithDolphin.Get(); OpenSaveFolderButton.IsEnabled = Directory.Exists(PathManager.SaveFolderPath); } @@ -50,12 +53,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/VideoSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs index 74b65613..c840f87f 100644 --- a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs @@ -1,20 +1,25 @@ using Avalonia.Controls; using Avalonia.Interactivity; using WheelWizard.Models.Settings; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; +using WheelWizard.Views; 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 +43,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 +63,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 +75,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 +104,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..42a81d2c 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs @@ -11,20 +11,28 @@ using WheelWizard.Models.Settings; 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; using WheelWizard.Views.Popups.Generic; using Button = WheelWizard.Views.Components.Button; using SettingsResource = WheelWizard.Resources.Languages.Settings; 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!; + public WhWzSettings() { InitializeComponent(); @@ -49,7 +57,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 +78,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) @@ -248,7 +256,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 +288,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 +305,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 +334,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 +638,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 +665,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 +705,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 +727,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..30b1d003 100644 --- a/WheelWizard/Views/Pages/TestingPage.axaml.cs +++ b/WheelWizard/Views/Pages/TestingPage.axaml.cs @@ -3,7 +3,7 @@ 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; @@ -18,10 +18,13 @@ public partial class TestingPage : UserControlBase [Inject] private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + [Inject] + private ISettingsManager SettingsService { get; set; } = null!; + public TestingPage() { InitializeComponent(); - _launcher = new RrBetaLauncher(); + _launcher = App.Services.GetRequiredService(); UpdateStatusAsync(); } @@ -36,7 +39,7 @@ private async void UpdateStatusAsync() private void UpdateUi() { - var pathsReady = SettingsHelper.PathsSetupCorrectly(); + var pathsReady = SettingsService.PathsSetupCorrectly(); var isInstalled = _status == WheelWizardStatus.Ready; InstallButton.IsEnabled = pathsReady && !_isBusy; diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs index fc94c937..9e32fb60 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs @@ -8,7 +8,7 @@ using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; -using WheelWizard.Services.Settings; +using WheelWizard.Settings; 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,7 +113,7 @@ public int ActiveInfoSlideIndex } private int _currentUserIndex; - private static int FocussedUser => (int)SettingsManager.FOCUSSED_USER.Get(); + private int FocussedUser => SettingsService.FocussedUser.Get(); public UserProfilePage() { @@ -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) @@ -241,7 +244,7 @@ private void SetUserAsPrimary() if (FocussedUser == _currentUserIndex) return; - SettingsManager.FOCUSSED_USER.Set(_currentUserIndex); + SettingsService.Set(SettingsService.FOCUSSED_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); From a99abb0bb1c4ec87831fdac7d13f9bd49f5a78cd Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:50:13 +0100 Subject: [PATCH 02/15] split the Dolphin Installer. it still needs some changes. but it was not the goal of this branch but i had to clean up settings so it had to be moved away --- .gitignore | 3 + .../Features/LinuxDolphinInstallerTests.cs | 116 +++++++++++ .../DolphinInstallerExtensions.cs | 18 ++ .../LinuxCommandEnvironment.cs | 22 +++ .../DolphinInstaller/LinuxDolphinInstaller.cs | 79 ++++++++ .../DolphinInstaller/LinuxProcessService.cs | 109 +++++++++++ .../Settings/DolphinSettingManager.cs | 68 +++++-- .../Features/Settings/ISettingsServices.cs | 39 ++-- .../Settings/LinuxDolphinInstaller.cs | 184 ------------------ .../Features/Settings/SettingsExtensions.cs | 1 + .../Settings/SettingsLocalizationService.cs | 36 ++++ .../Features/Settings/SettingsManager.cs | 84 ++++---- .../Features/Settings/SettingsRuntime.cs | 26 +++ .../Settings/SettingsStartupInitializer.cs | 31 ++- .../Settings/SettingsValidationReport.cs | 31 +++ .../Features/Settings/WhWzSettingManager.cs | 55 ++++-- .../GameLicense/GameLicenseService.cs | 4 +- .../MiiManagement/MiiExtensions.cs | 6 +- WheelWizard/Program.cs | 6 +- .../Launcher/Helpers/DolphinLaunchHelper.cs | 4 +- WheelWizard/Services/LiveData/RRLiveRooms.cs | 30 +-- .../Services/LiveData/WhWzStatusManager.cs | 20 +- WheelWizard/Services/PathManager.cs | 4 +- .../Services/WiiManagement/WiiMoteSettings.cs | 6 +- WheelWizard/SetupExtensions.cs | 5 + WheelWizard/Views/NavigationManager.cs | 3 +- WheelWizard/Views/Pages/FriendsPage.axaml.cs | 4 +- WheelWizard/Views/Pages/HomePage.axaml.cs | 55 +++--- .../Views/Pages/RoomDetailsPage.axaml.cs | 2 +- .../Views/Pages/Settings/AppInfo.axaml.cs | 5 +- .../Pages/Settings/WhWzSettings.axaml.cs | 16 +- WheelWizard/Views/Pages/TestingPage.axaml.cs | 12 +- .../Views/Pages/UserProfilePage.axaml.cs | 10 +- 33 files changed, 743 insertions(+), 351 deletions(-) create mode 100644 WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs create mode 100644 WheelWizard/Features/DolphinInstaller/DolphinInstallerExtensions.cs create mode 100644 WheelWizard/Features/DolphinInstaller/LinuxCommandEnvironment.cs create mode 100644 WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs create mode 100644 WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs delete mode 100644 WheelWizard/Features/Settings/LinuxDolphinInstaller.cs create mode 100644 WheelWizard/Features/Settings/SettingsLocalizationService.cs create mode 100644 WheelWizard/Features/Settings/SettingsRuntime.cs create mode 100644 WheelWizard/Features/Settings/SettingsValidationReport.cs 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..972586a9 --- /dev/null +++ b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs @@ -0,0 +1,116 @@ +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 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/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..8226bc61 --- /dev/null +++ b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs @@ -0,0 +1,79 @@ +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)); + if (launchResult.IsFailure) + return launchResult.Error; + + return Ok(); + } +} diff --git a/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs b/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs new file mode 100644 index 00000000..97efcd12 --- /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 int percent)) + progress?.Report(percent); + } +} diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 3f137799..4178d9b6 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -8,57 +8,83 @@ public class DolphinSettingManager : IDolphinSettingManager { private static string ConfigFolderPath(string fileName) => Path.Combine(PathManager.ConfigFolderPath, fileName); + private readonly object _syncRoot = new(); + private readonly object _fileIoSync = new(); private bool _loaded; private readonly List _settings = []; public void RegisterSetting(DolphinSetting setting) { - if (_loaded) - return; + lock (_syncRoot) + { + if (_loaded) + return; - _settings.Add(setting); + _settings.Add(setting); + } } public void SaveSettings(DolphinSetting invokingSetting) { - // TODO: This method definitely has to be optimized - if (!_loaded) - return; + List settingsSnapshot; + lock (_syncRoot) + { + // TODO: This method definitely has to be optimized + if (!_loaded) + return; - foreach (var setting in _settings) + settingsSnapshot = [.. _settings]; + } + + lock (_fileIoSync) { - ChangeIniSettings(setting.FileName, setting.Section, setting.Name, setting.GetStringValue()); + foreach (var setting in settingsSnapshot) + { + 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; + 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() { - if (_loaded) - return; + List settingsSnapshot; + lock (_syncRoot) + { + if (_loaded) + return; + + _loaded = true; + settingsSnapshot = [.. _settings]; + } 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) + lock (_fileIoSync) { - 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 + 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 + } } - - _loaded = true; } private static string[]? ReadIniFile(string fileName) diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs index 004c46f7..d16e046c 100644 --- a/WheelWizard/Features/Settings/ISettingsServices.cs +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -26,7 +26,7 @@ public interface ITypedSetting Setting RawSetting { get; } } -public interface ISettingsManager +public interface IGeneralSettings { Setting USER_FOLDER_PATH { get; } Setting DOLPHIN_LOCATION { get; } @@ -34,7 +34,7 @@ public interface ISettingsManager Setting FORCE_WIIMOTE { get; } Setting LAUNCH_WITH_DOLPHIN { get; } Setting PREFERS_MODS_ROW_VIEW { get; } - Setting FOCUSSED_USER { get; } + Setting FOCUSED_USER { get; } Setting ENABLE_ANIMATIONS { get; } Setting TESTING_MODE_ENABLED { get; } Setting SAVED_WINDOW_SCALE { get; } @@ -42,29 +42,37 @@ public interface ISettingsManager 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; } - ITypedSetting UserFolderPath { get; } ITypedSetting DolphinLocation { get; } ITypedSetting GameLocation { get; } ITypedSetting ForceWiimote { get; } ITypedSetting LaunchWithDolphin { get; } ITypedSetting PrefersModsRowView { get; } - ITypedSetting FocussedUser { get; } + ITypedSetting FocusedUser { get; } ITypedSetting EnableAnimations { get; } ITypedSetting TestingModeEnabled { get; } ITypedSetting SavedWindowScale { get; } ITypedSetting RemoveBlur { get; } ITypedSetting WwLanguage { get; } +} + +public interface IDolphinSettings +{ + 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; } ITypedSetting MacAddress { get; } +} + +public interface ISettingsManager : IGeneralSettings, IDolphinSettings +{ + OperationResult ValidateCorePathSettings(); T Get(Setting setting); bool Set(Setting setting, T value, bool skipSave = false); @@ -76,3 +84,8 @@ public interface ISettingsStartupInitializer { void Initialize(); } + +public interface ISettingsLocalizationService +{ + void Initialize(); +} diff --git a/WheelWizard/Features/Settings/LinuxDolphinInstaller.cs b/WheelWizard/Features/Settings/LinuxDolphinInstaller.cs deleted file mode 100644 index a6cccd7f..00000000 --- a/WheelWizard/Features/Settings/LinuxDolphinInstaller.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; -using Avalonia.Threading; -using WheelWizard.Helpers; -using WheelWizard.Views.Popups.Generic; - -namespace WheelWizard.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); - if (process == null) - return false; - - 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); - if (process == null) - return -1; - - // 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/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index 7b6440de..3a2e7267 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -7,6 +7,7 @@ public static IServiceCollection AddSettings(this IServiceCollection services) 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..618684c9 --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsLocalizationService.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using WheelWizard.Models.Settings; + +namespace WheelWizard.Settings; + +public sealed class SettingsLocalizationService(ISettingsManager settingsManager) : ISettingsLocalizationService, ISettingListener +{ + private readonly object _syncRoot = new(); + private bool _initialized; + + public void Initialize() + { + lock (_syncRoot) + { + if (_initialized) + return; + + settingsManager.WW_LANGUAGE.Subscribe(this); + ApplyCulture(); + _initialized = true; + } + } + + public void OnSettingChanged(Setting setting) + { + if (setting == settingsManager.WW_LANGUAGE) + ApplyCulture(); + } + + private void ApplyCulture() + { + var newCulture = new CultureInfo(settingsManager.WwLanguage.Get()); + CultureInfo.CurrentCulture = newCulture; + CultureInfo.CurrentUICulture = newCulture; + } +} diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 24a937c8..1eb2e5c1 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -1,5 +1,5 @@ -using System.Globalization; using System.Runtime.InteropServices; +using WheelWizard.DolphinInstaller; using WheelWizard.Helpers; using WheelWizard.Models.Enums; using WheelWizard.Models.Settings; @@ -7,23 +7,30 @@ namespace WheelWizard.Settings; -public class SettingsManager : ISettingsManager, ISettingListener +public class SettingsManager : ISettingsManager { + private readonly object _syncRoot = new(); private readonly IWhWzSettingManager _whWzSettingManager; private readonly IDolphinSettingManager _dolphinSettingManager; + private readonly ILinuxDolphinInstaller _linuxDolphinInstaller; private readonly Setting _dolphinCompilationMode; private readonly Setting _dolphinCompileShadersAtStart; private readonly Setting _dolphinSsaa; private readonly Setting _dolphinMsaa; - private bool _hasInitializedLanguageSync; + private bool _hasLoadedSettings; private double _internalScale = -1.0; - public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager) + public SettingsManager( + IWhWzSettingManager whWzSettingManager, + IDolphinSettingManager dolphinSettingManager, + ILinuxDolphinInstaller linuxDolphinInstaller + ) { _whWzSettingManager = whWzSettingManager; _dolphinSettingManager = dolphinSettingManager; + _linuxDolphinInstaller = linuxDolphinInstaller; DOLPHIN_LOCATION = RegisterWhWz( CreateWhWzSetting(typeof(string), "DolphinLocation", "") @@ -37,7 +44,9 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !LinuxDolphinInstaller.IsDolphinInstalledInFlatpak()) + if ( + PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak() + ) { return false; } @@ -65,14 +74,10 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa 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). + // this only really works as expected if certain conditions are met. 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... + // In this case, Dolphin would use `EMBEDDED_USER_DIR` (portable `user` directory). if (FileHelper.DirectoryExists("user")) return false; @@ -105,7 +110,7 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa FORCE_WIIMOTE = RegisterWhWz(CreateWhWzSetting(typeof(bool), "ForceWiimote", false)); LAUNCH_WITH_DOLPHIN = RegisterWhWz(CreateWhWzSetting(typeof(bool), "LaunchWithDolphin", false)); PREFERS_MODS_ROW_VIEW = RegisterWhWz(CreateWhWzSetting(typeof(bool), "PrefersModsRowView", true)); - FOCUSSED_USER = RegisterWhWz( + FOCUSED_USER = RegisterWhWz( CreateWhWzSetting(typeof(int), "FavoriteUser", 0).SetValidation(value => (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4) ); @@ -201,7 +206,7 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa ForceWiimote = new TypedSetting(FORCE_WIIMOTE); LaunchWithDolphin = new TypedSetting(LAUNCH_WITH_DOLPHIN); PrefersModsRowView = new TypedSetting(PREFERS_MODS_ROW_VIEW); - FocussedUser = new TypedSetting(FOCUSSED_USER); + FocusedUser = new TypedSetting(FOCUSED_USER); EnableAnimations = new TypedSetting(ENABLE_ANIMATIONS); TestingModeEnabled = new TypedSetting(TESTING_MODE_ENABLED); SavedWindowScale = new TypedSetting(SAVED_WINDOW_SCALE); @@ -216,7 +221,7 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa public Setting FORCE_WIIMOTE { get; } public Setting LAUNCH_WITH_DOLPHIN { get; } public Setting PREFERS_MODS_ROW_VIEW { get; } - public Setting FOCUSSED_USER { get; } + public Setting FOCUSED_USER { get; } public Setting ENABLE_ANIMATIONS { get; } public Setting TESTING_MODE_ENABLED { get; } public Setting SAVED_WINDOW_SCALE { get; } @@ -240,7 +245,7 @@ public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingMa public ITypedSetting ForceWiimote { get; } public ITypedSetting LaunchWithDolphin { get; } public ITypedSetting PrefersModsRowView { get; } - public ITypedSetting FocussedUser { get; } + public ITypedSetting FocusedUser { get; } public ITypedSetting EnableAnimations { get; } public ITypedSetting TestingModeEnabled { get; } public ITypedSetting SavedWindowScale { get; } @@ -267,33 +272,46 @@ public bool Set(Setting setting, T value, bool skipSave = false) public bool PathsSetupCorrectly() { - return USER_FOLDER_PATH.IsValid() && DOLPHIN_LOCATION.IsValid() && GAME_LOCATION.IsValid(); + var reportResult = ValidateCorePathSettings(); + return reportResult.IsSuccess && reportResult.Value.IsValid; } - public void LoadSettings() + public OperationResult ValidateCorePathSettings() { - _whWzSettingManager.LoadSettings(); - _dolphinSettingManager.LoadSettings(); + try + { + var issues = new List(); - if (_hasInitializedLanguageSync) - return; + if (!USER_FOLDER_PATH.IsValid()) + issues.Add(new(SettingsValidationCode.InvalidUserFolderPath, USER_FOLDER_PATH.Name, "User folder path is invalid.")); - WW_LANGUAGE.Subscribe(this); - OnWheelWizardLanguageChange(); - _hasInitializedLanguageSync = true; - } + if (!DOLPHIN_LOCATION.IsValid()) + issues.Add( + new(SettingsValidationCode.InvalidDolphinLocation, DOLPHIN_LOCATION.Name, "Dolphin path or command is invalid.") + ); - public void OnSettingChanged(Setting setting) - { - if (setting == WW_LANGUAGE) - OnWheelWizardLanguageChange(); + 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); + } } - private void OnWheelWizardLanguageChange() + public void LoadSettings() { - var newCulture = new CultureInfo(WwLanguage.Get()); - CultureInfo.CurrentCulture = newCulture; - CultureInfo.CurrentUICulture = newCulture; + lock (_syncRoot) + { + if (_hasLoadedSettings) + return; + + _whWzSettingManager.LoadSettings(); + _dolphinSettingManager.LoadSettings(); + _hasLoadedSettings = true; + } } private WhWzSetting CreateWhWzSetting(Type valueType, string name, object defaultValue) diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs new file mode 100644 index 00000000..9b5ac85e --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -0,0 +1,26 @@ +namespace WheelWizard.Settings; + +public static class SettingsRuntime +{ + private static readonly object SyncRoot = new(); + private static ISettingsManager? _current; + + public static ISettingsManager Current + { + get + { + lock (SyncRoot) + { + return _current ?? throw new InvalidOperationException("Settings runtime has not been initialized yet."); + } + } + } + + public static void Initialize(ISettingsManager settingsManager) + { + lock (SyncRoot) + { + _current = settingsManager; + } + } +} diff --git a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs index d2cb8051..04400065 100644 --- a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs +++ b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs @@ -1,9 +1,38 @@ +using Microsoft.Extensions.Logging; + namespace WheelWizard.Settings; -public sealed class SettingsStartupInitializer(ISettingsManager settingsManager) : ISettingsStartupInitializer +public sealed class SettingsStartupInitializer( + ISettingsManager settingsManager, + ISettingsLocalizationService localizationService, + ILogger logger +) : ISettingsStartupInitializer { public void Initialize() { + 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) + { + 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/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs index bc7474e7..56b727dc 100644 --- a/WheelWizard/Features/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -8,41 +8,66 @@ namespace WheelWizard.Settings; public class WhWzSettingManager(ILogger logger) : IWhWzSettingManager { + private readonly object _syncRoot = new(); + private readonly object _fileIoSync = new(); private bool _loaded; private readonly Dictionary _settings = new(); public void RegisterSetting(WhWzSetting setting) { - if (_loaded) - return; + lock (_syncRoot) + { + if (_loaded) + return; - _settings[setting.Name] = setting; + _settings[setting.Name] = setting; + } } public void SaveSettings(WhWzSetting invokingSetting) { - if (!_loaded) - return; + Dictionary settingsSnapshot; + lock (_syncRoot) + { + if (!_loaded) + return; + + settingsSnapshot = new(_settings); + } var settingsToSave = new Dictionary(); - foreach (var (name, setting) in _settings) + foreach (var (name, setting) in settingsSnapshot) { settingsToSave[name] = setting.Get(); } var jsonString = JsonSerializer.Serialize(settingsToSave, new JsonSerializerOptions { WriteIndented = true }); - FileHelper.WriteAllTextSafe(PathManager.WheelWizardConfigFilePath, jsonString); + lock (_fileIoSync) + { + FileHelper.WriteAllTextSafe(PathManager.WheelWizardConfigFilePath, jsonString); + } } public void LoadSettings() { - if (_loaded) - return; + 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) + { + jsonString = FileHelper.ReadAllTextSafe(PathManager.WheelWizardConfigFilePath); + } - _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; @@ -54,10 +79,12 @@ public void LoadSettings() foreach (var kvp in loadedSettings) { - if (!_settings.TryGetValue(kvp.Key, out var setting)) + settingsSnapshot.TryGetValue(kvp.Key, out var setting); + + if (setting == null) continue; - setting.SetFromJson(kvp.Value); + setting.SetFromJson(kvp.Value, skipSave: true); } } catch (JsonException e) diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index c7e62204..ec7eff59 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -134,9 +134,9 @@ ISettingsManager settingsManager /// /// Returns the "focused" or currently active license/user as determined by the Settings. /// - public LicenseProfile ActiveUser => Licenses.Users[_settingsManager.FocussedUser.Get()]; + public LicenseProfile ActiveUser => Licenses.Users[_settingsManager.FocusedUser.Get()]; - public List ActiveCurrentFriends => Licenses.Users[_settingsManager.FocussedUser.Get()].Friends; + public List ActiveCurrentFriends => Licenses.Users[_settingsManager.FocusedUser.Get()].Friends; public LicenseCollection LicenseCollection => Licenses; diff --git a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs index 63156681..73089a58 100644 --- a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs +++ b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs @@ -1,13 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; -using WheelWizard.Settings; -using WheelWizard.Views; +using WheelWizard.Settings; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; namespace WheelWizard.WiiManagement.MiiManagement; public static class MiiExtensions { - private static ISettingsManager Settings => App.Services.GetRequiredService(); + 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; diff --git a/WheelWizard/Program.cs b/WheelWizard/Program.cs index e5f02eb6..4f0f8e36 100644 --- a/WheelWizard/Program.cs +++ b/WheelWizard/Program.cs @@ -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) { - App.Services.GetRequiredService().Initialize(); + serviceProvider.GetRequiredService().Initialize(); UrlProtocolManager.SetWhWzScheme(); } } diff --git a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs index 4bd6a347..aab0fc7d 100644 --- a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs +++ b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs @@ -1,17 +1,15 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; -using Microsoft.Extensions.DependencyInjection; using WheelWizard.Helpers; using WheelWizard.Settings; -using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Services.Launcher.Helpers; public static class DolphinLaunchHelper { - private static ISettingsManager Settings => App.Services.GetRequiredService(); + private static ISettingsManager Settings => SettingsRuntime.Current; public static void KillDolphin() //dont tell PETA { 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/PathManager.cs b/WheelWizard/Services/PathManager.cs index d2198537..9ee155a7 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -1,8 +1,6 @@ using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; using WheelWizard.Helpers; using WheelWizard.Settings; -using WheelWizard.Views; #if WINDOWS using Microsoft.Win32; #endif @@ -11,7 +9,7 @@ namespace WheelWizard.Services; public static class PathManager { - private static ISettingsManager Settings => App.Services.GetRequiredService(); + private static ISettingsManager Settings => SettingsRuntime.Current; // IMPORTANT: To keep things consistent all paths should be Attrib expressions, // and either end with `FilePath` or `FolderPath` diff --git a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs index 90da4b64..91b9251d 100644 --- a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs +++ b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs @@ -1,13 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; -using WheelWizard.Helpers; +using WheelWizard.Helpers; using WheelWizard.Settings; -using WheelWizard.Views; namespace WheelWizard.Services.WiiManagement; public static class WiiMoteSettings { - private static ISettingsManager Settings => App.Services.GetRequiredService(); + private static ISettingsManager Settings => SettingsRuntime.Current; private const string WiimoteSection = "[Wiimote1]"; private const string SourceParameter = "Source"; diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index cce19b50..88f5c6bc 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -6,11 +6,13 @@ 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; @@ -27,6 +29,7 @@ public static class SetupExtensions public static void AddWheelWizardServices(this IServiceCollection services) { // Features + services.AddDolphinInstaller(); services.AddSettings(); services.AddCustomCharacters(); services.AddAutoUpdating(); @@ -53,5 +56,7 @@ public static void AddWheelWizardServices(this IServiceCollection services) services.AddTransient(typeof(IApiCaller<>), typeof(ApiCaller<>)); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); } } diff --git a/WheelWizard/Views/NavigationManager.cs b/WheelWizard/Views/NavigationManager.cs index d270aef6..9ec702bf 100644 --- a/WheelWizard/Views/NavigationManager.cs +++ b/WheelWizard/Views/NavigationManager.cs @@ -1,13 +1,12 @@ using System.Globalization; using Avalonia.Controls; -using Microsoft.Extensions.DependencyInjection; using WheelWizard.Settings; namespace WheelWizard.Views; public static class NavigationManager { - private static ISettingsManager Settings => App.Services.GetRequiredService(); + private static ISettingsManager Settings => SettingsRuntime.Current; public static void NavigateTo(Type pageType, params object?[] args) { diff --git a/WheelWizard/Views/Pages/FriendsPage.axaml.cs b/WheelWizard/Views/Pages/FriendsPage.axaml.cs index ad064955..ff36a776 100644 --- a/WheelWizard/Views/Pages/FriendsPage.axaml.cs +++ b/WheelWizard/Views/Pages/FriendsPage.axaml.cs @@ -143,7 +143,7 @@ private void SortByDropdown_OnSelectionChanged(object? sender, SelectionChangedE private async void AddFriend_OnClick(object? sender, RoutedEventArgs e) { - var focusedUserIndex = SettingsService.FocussedUser.Get(); + var focusedUserIndex = SettingsService.FocusedUser.Get(); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); @@ -318,7 +318,7 @@ private void RemoveFriend_OnClick(object sender, RoutedEventArgs e) if (string.IsNullOrWhiteSpace(selectedPlayer.FriendCode)) return; - var focusedUserIndex = SettingsService.FocussedUser.Get(); + var focusedUserIndex = SettingsService.FocusedUser.Get(); 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 e06a3135..e14dcfa6 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -4,13 +4,13 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; -using Microsoft.Extensions.DependencyInjection; using Testably.Abstractions; using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; using WheelWizard.Services.Launcher; using WheelWizard.Services.Launcher.Helpers; using WheelWizard.Settings; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Components; using WheelWizard.Views.Pages.Settings; using Button = WheelWizard.Views.Components.Button; @@ -19,23 +19,27 @@ namespace WheelWizard.Views.Pages; public partial class HomePage : UserControlBase { - private static ISettingsManager SettingsService => App.Services.GetRequiredService(); - 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 = - [ - App.Services.GetRequiredService(), - //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), @@ -70,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(); } @@ -91,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(); @@ -143,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() @@ -157,7 +160,7 @@ private void DisableAllButtonsTemporarily() { Dispatcher.UIThread.InvokeAsync(() => { - SetButtonState(currentButtonState); + SetButtonState(CurrentButtonState); return CompleteGrid.IsEnabled = true; }); }); diff --git a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs index 0ffd67eb..54283f1b 100644 --- a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs +++ b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs @@ -135,7 +135,7 @@ private async void AddFriend_OnClick(object sender, RoutedEventArgs e) return; } - var focusedUserIndex = SettingsService.FocussedUser.Get(); + var focusedUserIndex = SettingsService.FocusedUser.Get(); 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..b1751f89 100644 --- a/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs @@ -11,6 +11,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 +49,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/WhWzSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs index 42a81d2c..9d0221e0 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Threading; using HarfBuzzSharp; using Serilog; +using WheelWizard.DolphinInstaller; using WheelWizard.Helpers; using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; @@ -33,6 +34,9 @@ public partial class WhWzSettings : UserControlBase [Inject] private IDolphinSettingManager DolphinSettingsService { get; set; } = null!; + [Inject] + private ILinuxDolphinInstaller LinuxDolphinInstallerService { get; set; } = null!; + public WhWzSettings() { InitializeComponent(); @@ -146,11 +150,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; } @@ -219,7 +227,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) diff --git a/WheelWizard/Views/Pages/TestingPage.axaml.cs b/WheelWizard/Views/Pages/TestingPage.axaml.cs index 30b1d003..aada7ffc 100644 --- a/WheelWizard/Views/Pages/TestingPage.axaml.cs +++ b/WheelWizard/Views/Pages/TestingPage.axaml.cs @@ -2,7 +2,6 @@ using WheelWizard.CustomDistributions; using WheelWizard.Models.Enums; using WheelWizard.Services.Launcher; -using WheelWizard.Services.Launcher.Helpers; using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Generic; @@ -11,7 +10,6 @@ namespace WheelWizard.Views.Pages; public partial class TestingPage : UserControlBase { - private readonly ILauncher _launcher; private WheelWizardStatus _status = WheelWizardStatus.Loading; private bool _isBusy; @@ -21,10 +19,12 @@ public partial class TestingPage : UserControlBase [Inject] private ISettingsManager SettingsService { get; set; } = null!; + [Inject] + private RrBetaLauncher LauncherService { get; set; } = null!; + public TestingPage() { InitializeComponent(); - _launcher = App.Services.GetRequiredService(); UpdateStatusAsync(); } @@ -33,7 +33,7 @@ private async void UpdateStatusAsync() _status = WheelWizardStatus.Loading; UpdateUi(); - _status = await _launcher.GetCurrentStatus(); + _status = await LauncherService.GetCurrentStatus(); UpdateUi(); } @@ -70,7 +70,7 @@ private async void InstallButton_OnClick(object? sender, RoutedEventArgs e) _isBusy = true; UpdateUi(); - await _launcher.Install(); + await LauncherService.Install(); _isBusy = false; UpdateStatusAsync(); @@ -109,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 9e32fb60..d484fb5c 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs @@ -113,13 +113,13 @@ public int ActiveInfoSlideIndex } private int _currentUserIndex; - private int FocussedUser => SettingsService.FocussedUser.Get(); + private int FocusedUser => SettingsService.FocusedUser.Get(); public UserProfilePage() { InitializeComponent(); ResetMiiTopBar(); - ViewMii(FocussedUser); + ViewMii(FocusedUser); PopulateRegions(); UpdatePage(); DataContext = this; @@ -201,7 +201,7 @@ private void ResetMiiTopBar() private void UpdatePage() { - PrimaryCheckBox.IsChecked = FocussedUser == _currentUserIndex; + PrimaryCheckBox.IsChecked = FocusedUser == _currentUserIndex; currentPlayer = GameLicenseService.GetUserData(_currentUserIndex); CurrentFriendCode = currentPlayer.FriendCode; @@ -241,10 +241,10 @@ private void ViewMii(int? mii = null) private void SetUserAsPrimary() { - if (FocussedUser == _currentUserIndex) + if (FocusedUser == _currentUserIndex) return; - SettingsService.Set(SettingsService.FOCUSSED_USER, _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, From 31a18265dc70226d6864c71c4c6bc09cc8550bad Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:21:02 +0100 Subject: [PATCH 03/15] add settings singal bus and move ofver to the IFileSystem service --- .../Settings/DolphinSettingManager.cs | 31 +++-- .../Features/Settings/SettingsExtensions.cs | 10 ++ .../Settings/SettingsLocalizationService.cs | 11 +- .../Features/Settings/SettingsManager.cs | 25 ++-- .../Features/Settings/SettingsRuntime.cs | 3 + .../Features/Settings/SettingsSignalBus.cs | 125 ++++++++++++++++++ .../Settings/SettingsStartupInitializer.cs | 2 + .../Features/Settings/WhWzSettingManager.cs | 45 +++++-- WheelWizard/Helpers/FileHelper.cs | 1 + WheelWizard/Models/Settings/Setting.cs | 6 + WheelWizard/Models/Settings/VirtualSetting.cs | 29 ++-- 11 files changed, 244 insertions(+), 44 deletions(-) create mode 100644 WheelWizard/Features/Settings/SettingsSignalBus.cs diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 4178d9b6..1667cce9 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -1,10 +1,10 @@ -using WheelWizard.Helpers; +using System.IO.Abstractions; using WheelWizard.Models.Settings; using WheelWizard.Services; namespace WheelWizard.Settings; -public class DolphinSettingManager : IDolphinSettingManager +public class DolphinSettingManager(IFileSystem fileSystem) : IDolphinSettingManager { private static string ConfigFolderPath(string fileName) => Path.Combine(PathManager.ConfigFolderPath, fileName); @@ -69,7 +69,7 @@ public void LoadSettings() settingsSnapshot = [.. _settings]; } - if (!FileHelper.DirectoryExists(PathManager.ConfigFolderPath)) + if (!fileSystem.Directory.Exists(PathManager.ConfigFolderPath)) return; // TODO: This method can maybe be optimized in the future, since now it reads the file for every setting @@ -87,14 +87,23 @@ public void LoadSettings() } } - private static string[]? ReadIniFile(string fileName) + private string[]? ReadIniFile(string fileName) { var filePath = ConfigFolderPath(fileName); - var lines = FileHelper.ReadAllLinesSafe(filePath); - return lines; + if (!fileSystem.File.Exists(filePath)) + return null; + + try + { + return fileSystem.File.ReadAllLines(filePath); + } + catch + { + return null; + } } - private static string? ReadIniSetting(string fileName, string section, string settingToRead) + private string? ReadIniSetting(string fileName, string section, string settingToRead) { var lines = ReadIniFile(fileName); if (lines == null) @@ -125,7 +134,7 @@ public void LoadSettings() } // 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) + private void ChangeIniSettings(string fileName, string section, string settingToChange, string value) { var lines = ReadIniFile(fileName)?.ToList(); if (lines == null) @@ -136,7 +145,7 @@ private static void ChangeIniSettings(string fileName, string section, string se { lines.Add($"[{section}]"); lines.Add($"{settingToChange} = {value}"); - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); + fileSystem.File.WriteAllLines(ConfigFolderPath(fileName), lines); return; } @@ -150,12 +159,12 @@ private static void ChangeIniSettings(string fileName, string section, string se continue; lines[i] = $"{settingToChange} = {value}"; - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); + 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}"); - FileHelper.WriteAllLines(ConfigFolderPath(fileName), lines); + fileSystem.File.WriteAllLines(ConfigFolderPath(fileName), lines); } } diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index 3a2e7267..2379d810 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -4,6 +4,16 @@ public static class SettingsExtensions { public static IServiceCollection AddSettings(this IServiceCollection services) { + // NExt step: + // - we created Settings Singal bus, make sure it is also being used everywhere such that you dont subseribve or unsubscribe to a setting model anymore. + // - Remove/ move the model to the settings add it in Domains. make sure there is no ugly remenance somewhere in the depricated codebase anymore + // - i saw setting still use path manager, can we simply replace that iwt the IFileSystem? or does that need some more work + // - investigate if localization service really has to be its own setting service or if it can be combined with the rest of the settings. OR maybe even better, that it can become its own localization feature (that uses setting manager) + // - look at the settings initializer, and if we really need that or if there is a better way + + // look at all the code as high level overview + + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/WheelWizard/Features/Settings/SettingsLocalizationService.cs b/WheelWizard/Features/Settings/SettingsLocalizationService.cs index 618684c9..1f8c1fae 100644 --- a/WheelWizard/Features/Settings/SettingsLocalizationService.cs +++ b/WheelWizard/Features/Settings/SettingsLocalizationService.cs @@ -1,12 +1,13 @@ using System.Globalization; -using WheelWizard.Models.Settings; namespace WheelWizard.Settings; -public sealed class SettingsLocalizationService(ISettingsManager settingsManager) : ISettingsLocalizationService, ISettingListener +public sealed class SettingsLocalizationService(ISettingsManager settingsManager, ISettingsSignalBus settingsSignalBus) + : ISettingsLocalizationService { private readonly object _syncRoot = new(); private bool _initialized; + private IDisposable? _subscription; public void Initialize() { @@ -15,15 +16,15 @@ public void Initialize() if (_initialized) return; - settingsManager.WW_LANGUAGE.Subscribe(this); + _subscription = settingsSignalBus.Subscribe(OnSignal); ApplyCulture(); _initialized = true; } } - public void OnSettingChanged(Setting setting) + private void OnSignal(SettingChangedSignal signal) { - if (setting == settingsManager.WW_LANGUAGE) + if (signal.Setting == settingsManager.WW_LANGUAGE) ApplyCulture(); } diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 1eb2e5c1..035d0453 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using System.Runtime.InteropServices; using WheelWizard.DolphinInstaller; using WheelWizard.Helpers; @@ -13,6 +14,7 @@ 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; @@ -25,12 +27,14 @@ public class SettingsManager : ISettingsManager public SettingsManager( IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager, - ILinuxDolphinInstaller linuxDolphinInstaller + ILinuxDolphinInstaller linuxDolphinInstaller, + IFileSystem fileSystem ) { _whWzSettingManager = whWzSettingManager; _dolphinSettingManager = dolphinSettingManager; _linuxDolphinInstaller = linuxDolphinInstaller; + _fileSystem = fileSystem; DOLPHIN_LOCATION = RegisterWhWz( CreateWhWzSetting(typeof(string), "DolphinLocation", "") @@ -55,7 +59,7 @@ ILinuxDolphinInstaller linuxDolphinInstaller return EnvHelper.IsValidUnixCommand(pathOrCommand); } - return FileHelper.FileExists(pathOrCommand); + return _fileSystem.File.Exists(pathOrCommand); }) ); @@ -64,7 +68,7 @@ ILinuxDolphinInstaller linuxDolphinInstaller .SetValidation(value => { var userFolderPath = value as string ?? string.Empty; - if (!FileHelper.DirectoryExists(userFolderPath)) + if (!_fileSystem.Directory.Exists(userFolderPath)) return false; string dolphinLocation = Get(DOLPHIN_LOCATION); @@ -78,11 +82,11 @@ ILinuxDolphinInstaller linuxDolphinInstaller if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && PathManager.IsLinuxDolphinConfigSplit()) { // In this case, Dolphin would use `EMBEDDED_USER_DIR` (portable `user` directory). - if (FileHelper.DirectoryExists("user")) + if (_fileSystem.Directory.Exists("user")) return false; // The Dolphin executable directory with `portable.txt` case - if (FileHelper.FileExists(Path.Combine(PathManager.GetDolphinExeDirectory(), "portable.txt"))) + 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 @@ -95,7 +99,10 @@ ILinuxDolphinInstaller linuxDolphinInstaller return false; // `~/.dolphin-emu` would be used if it exists - if (!PathManager.IsFlatpakDolphinFilePath() && FileHelper.DirectoryExists(PathManager.LinuxDolphinLegacyFolderPath)) + if ( + !PathManager.IsFlatpakDolphinFilePath() + && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath) + ) return false; } @@ -105,7 +112,7 @@ ILinuxDolphinInstaller linuxDolphinInstaller GAME_LOCATION = RegisterWhWz( CreateWhWzSetting(typeof(string), "GameLocation", "") - .SetValidation(value => FileHelper.FileExists(value as string ?? string.Empty)) + .SetValidation(value => _fileSystem.File.Exists(value as string ?? string.Empty)) ); FORCE_WIIMOTE = RegisterWhWz(CreateWhWzSetting(typeof(bool), "ForceWiimote", false)); LAUNCH_WITH_DOLPHIN = RegisterWhWz(CreateWhWzSetting(typeof(bool), "LaunchWithDolphin", false)); @@ -129,12 +136,12 @@ ILinuxDolphinInstaller linuxDolphinInstaller NAND_ROOT_PATH = RegisterDolphin( CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "") - .SetValidation(value => Directory.Exists(value as string ?? string.Empty)) + .SetValidation(value => _fileSystem.Directory.Exists(value as string ?? string.Empty)) ); LOAD_PATH = RegisterDolphin( CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "LoadPath"), "") - .SetValidation(value => Directory.Exists(value as string ?? string.Empty)) + .SetValidation(value => _fileSystem.Directory.Exists(value as string ?? string.Empty)) ); VSYNC = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Hardware", "VSync"), false)); diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs index 9b5ac85e..ba0c5e2a 100644 --- a/WheelWizard/Features/Settings/SettingsRuntime.cs +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -1,5 +1,8 @@ namespace WheelWizard.Settings; +// TODO: This file is temporary. +// it serves as a bridge for legacy code that cannot get constructor injection (like PathManager or NaviagtionManager). +// Once those are all one day migrated or removed, we can and MUST remove this Runtime public static class SettingsRuntime { private static readonly object SyncRoot = new(); diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs new file mode 100644 index 00000000..1c5cbaf1 --- /dev/null +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -0,0 +1,125 @@ +using WheelWizard.Models.Settings; + +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 : ISettingsSignalBus +{ + private readonly object _syncRoot = new(); + private readonly Dictionary> _subscribers = []; + private long _nextSubscriberId; + + public IDisposable Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + long id; + lock (_syncRoot) + { + id = _nextSubscriberId++; + _subscribers[id] = handler; + } + + return new Subscription(this, id); + } + + public void Publish(Setting setting) + { + Action[] handlers; + lock (_syncRoot) + { + handlers = [.. _subscribers.Values]; + } + + var signal = new SettingChangedSignal(setting); + foreach (var handler in handlers) + { + handler(signal); + } + } + + private void Unsubscribe(long subscriberId) + { + lock (_syncRoot) + { + _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; + } + } +} + +public static class SettingsSignalRuntime +{ + private static readonly object SyncRoot = new(); + private static ISettingsSignalBus? _current; + private static readonly List> PendingInitializers = []; + + public static void Initialize(ISettingsSignalBus signalBus) + { + ArgumentNullException.ThrowIfNull(signalBus); + + List> callbacksToRun; + lock (SyncRoot) + { + _current = signalBus; + callbacksToRun = [.. PendingInitializers]; + PendingInitializers.Clear(); + } + + foreach (var callback in callbacksToRun) + { + callback(signalBus); + } + } + + public static void OnInitialized(Action callback) + { + ArgumentNullException.ThrowIfNull(callback); + + ISettingsSignalBus? signalBus; + lock (SyncRoot) + { + signalBus = _current; + if (signalBus == null) + { + PendingInitializers.Add(callback); + return; + } + } + + callback(signalBus); + } + + public static void Publish(Setting setting) + { + ISettingsSignalBus? signalBus; + lock (SyncRoot) + { + signalBus = _current; + } + + signalBus?.Publish(setting); + } +} diff --git a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs index 04400065..3b7e1c5d 100644 --- a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs +++ b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs @@ -4,12 +4,14 @@ 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(); diff --git a/WheelWizard/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs index 56b727dc..b0775999 100644 --- a/WheelWizard/Features/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -1,12 +1,12 @@ +using System.IO.Abstractions; using System.Text.Json; using Microsoft.Extensions.Logging; -using WheelWizard.Helpers; using WheelWizard.Models.Settings; using WheelWizard.Services; namespace WheelWizard.Settings; -public class WhWzSettingManager(ILogger logger) : IWhWzSettingManager +public class WhWzSettingManager(ILogger logger, IFileSystem fileSystem) : IWhWzSettingManager { private readonly object _syncRoot = new(); private readonly object _fileIoSync = new(); @@ -45,7 +45,19 @@ public void SaveSettings(WhWzSetting invokingSetting) var jsonString = JsonSerializer.Serialize(settingsToSave, new JsonSerializerOptions { WriteIndented = true }); lock (_fileIoSync) { - FileHelper.WriteAllTextSafe(PathManager.WheelWizardConfigFilePath, jsonString); + 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); + } } } @@ -65,7 +77,16 @@ public void LoadSettings() string? jsonString; lock (_fileIoSync) { - jsonString = FileHelper.ReadAllTextSafe(PathManager.WheelWizardConfigFilePath); + 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) @@ -79,12 +100,20 @@ public void LoadSettings() foreach (var kvp in loadedSettings) { - settingsSnapshot.TryGetValue(kvp.Key, out var setting); - - if (setting == null) + if (!settingsSnapshot.TryGetValue(kvp.Key, out var setting)) continue; - setting.SetFromJson(kvp.Value, skipSave: true); + 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) 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/Setting.cs b/WheelWizard/Models/Settings/Setting.cs index 183bf060..21d5d9ec 100644 --- a/WheelWizard/Models/Settings/Setting.cs +++ b/WheelWizard/Models/Settings/Setting.cs @@ -1,3 +1,5 @@ +using WheelWizard.Settings; + namespace WheelWizard.Models.Settings; public abstract class Setting @@ -59,8 +61,10 @@ public Setting SetForceSave(bool saveEvenIfNotValid) return this; } + [Obsolete("Use ISettingsSignalBus subscriptions instead of direct setting subscriptions.")] public bool Unsubscribe(ISettingListener dependent) => DependentVirtualSettings.Remove(dependent); + [Obsolete("Use ISettingsSignalBus subscriptions instead of direct setting subscriptions.")] public void Subscribe(ISettingListener dependent) { if (!DependentVirtualSettings.Contains(dependent)) @@ -69,6 +73,8 @@ public void Subscribe(ISettingListener dependent) protected void SignalChange() { + SettingsSignalRuntime.Publish(this); + foreach (var dependent in DependentVirtualSettings) { dependent.OnSettingChanged(this); diff --git a/WheelWizard/Models/Settings/VirtualSetting.cs b/WheelWizard/Models/Settings/VirtualSetting.cs index 03b20ec5..01a58883 100644 --- a/WheelWizard/Models/Settings/VirtualSetting.cs +++ b/WheelWizard/Models/Settings/VirtualSetting.cs @@ -1,18 +1,21 @@ +using WheelWizard.Settings; + namespace WheelWizard.Models.Settings; -public class VirtualSetting : Setting, ISettingListener +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,24 +51,28 @@ 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; + if (!_dependencies.Contains(signal.Setting)) + return; + SignalChange(); Recalculate(); } From 152e64f5b2ba8b8109081b5a32385e93f6ad1adc Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:16:44 +0100 Subject: [PATCH 04/15] small --- WheelWizard/Features/Settings/SettingsExtensions.cs | 7 ++++++- WheelWizard/Features/Settings/SettingsRuntime.cs | 2 +- WheelWizard/Features/Settings/SettingsSignalBus.cs | 1 - WheelWizard/Models/Settings/VirtualSetting.cs | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index 2379d810..d9221472 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -4,7 +4,12 @@ public static class SettingsExtensions { public static IServiceCollection AddSettings(this IServiceCollection services) { - // NExt step: + // TODO(naming-cleanup, later): + // - Prefix casing is inconsistent: `WhWz*` vs `WW_*` vs `Ww*` (example: `WhWzSettingManager`, `WW_LANGUAGE`, `WwLanguage`). + // - Some setting identifiers use all-caps while others use PascalCase (example: `MACADDRESS` vs `GAME_LOCATION` and typed props). + // - Domain type naming is mixed between generic and feature-specific terms (`Setting`, `WhWzSetting`, `DolphinSetting`). + + // Next step: // - we created Settings Singal bus, make sure it is also being used everywhere such that you dont subseribve or unsubscribe to a setting model anymore. // - Remove/ move the model to the settings add it in Domains. make sure there is no ugly remenance somewhere in the depricated codebase anymore // - i saw setting still use path manager, can we simply replace that iwt the IFileSystem? or does that need some more work diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs index ba0c5e2a..da766ea3 100644 --- a/WheelWizard/Features/Settings/SettingsRuntime.cs +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -1,6 +1,6 @@ namespace WheelWizard.Settings; -// TODO: This file is temporary. +// TODO: This file is temporary. // it serves as a bridge for legacy code that cannot get constructor injection (like PathManager or NaviagtionManager). // Once those are all one day migrated or removed, we can and MUST remove this Runtime public static class SettingsRuntime diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs index 1c5cbaf1..d4de388a 100644 --- a/WheelWizard/Features/Settings/SettingsSignalBus.cs +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -2,7 +2,6 @@ namespace WheelWizard.Settings; - public readonly record struct SettingChangedSignal(Setting Setting); public interface ISettingsSignalBus diff --git a/WheelWizard/Models/Settings/VirtualSetting.cs b/WheelWizard/Models/Settings/VirtualSetting.cs index 01a58883..fb0c2d57 100644 --- a/WheelWizard/Models/Settings/VirtualSetting.cs +++ b/WheelWizard/Models/Settings/VirtualSetting.cs @@ -73,7 +73,7 @@ private void OnSignal(SettingChangedSignal signal) if (!_dependencies.Contains(signal.Setting)) return; - SignalChange(); Recalculate(); + SignalChange(); } } From efd1ee0a7aeab8cbe5f2570d1509696b62f5c08c Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:22:29 +0100 Subject: [PATCH 05/15] how to add new settings --- .../Features/Settings/ADDING_SETTINGS.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 WheelWizard/Features/Settings/ADDING_SETTINGS.md diff --git a/WheelWizard/Features/Settings/ADDING_SETTINGS.md b/WheelWizard/Features/Settings/ADDING_SETTINGS.md new file mode 100644 index 00000000..4e076970 --- /dev/null +++ b/WheelWizard/Features/Settings/ADDING_SETTINGS.md @@ -0,0 +1,99 @@ +# Adding a Setting in WheelWizard + +This guide explains how to add a setting with the current Settings feature architecture. + +## Where Settings Live +- WheelWizard app settings (JSON): `WhWzSetting` +- Dolphin config settings (INI): `DolphinSetting` +- Derived/read-only computed settings: `VirtualSetting` + +Main wiring is in `WheelWizard/Features/Settings/SettingsManager.cs`. + +## Rules +- Add new setting logic in `Features/Settings`, not deprecated `Services/Helpers/Models/Utilities`. +- Use constructor-registered settings in `SettingsManager`. +- Use validation with `.SetValidation(...)` where possible. +- Prefer typed access (`ITypedSetting`) over raw `Setting` where possible. + +## 1) Add a new WhWz setting (stored in WheelWizard JSON) + +### Step A: Register in `SettingsManager` constructor +```csharp +MY_NEW_SETTING = RegisterWhWz( + CreateWhWzSetting(typeof(bool), "MyNewSetting", false) + .SetValidation(value => value is bool) +); +``` + +### Step B: Expose public properties in `SettingsManager` +```csharp +public Setting MY_NEW_SETTING { get; } +public ITypedSetting MyNewSetting { get; } +``` + +And initialize typed wrapper in constructor: +```csharp +MyNewSetting = new TypedSetting(MY_NEW_SETTING); +``` + +### Step C: Add interface members in `ISettingsServices.cs` +Add raw + typed members to the correct interface (`IGeneralSettings` or `IDolphinSettings`): +```csharp +Setting MY_NEW_SETTING { get; } +ITypedSetting MyNewSetting { get; } +``` + +## 2) Add a new Dolphin setting (stored in Dolphin INI) + +### Step A: Register in `SettingsManager` constructor +```csharp +MY_DOLPHIN_SETTING = RegisterDolphin( + CreateDolphinSetting(typeof(int), ("GFX.ini", "Settings", "MyDolphinKey"), 0) + .SetValidation(value => (int)(value ?? -1) >= 0) +); +``` + +### Step B: Expose public property in `SettingsManager` +```csharp +public Setting MY_DOLPHIN_SETTING { get; } +``` + +If you want typed access: +```csharp +public ITypedSetting MyDolphinSetting { get; } +MyDolphinSetting = new TypedSetting(MY_DOLPHIN_SETTING); +``` + +### Step C: Add to interface in `ISettingsServices.cs` +```csharp +Setting MY_DOLPHIN_SETTING { get; } +ITypedSetting MyDolphinSetting { get; } +``` + +## 3) Add a derived/computed setting (`VirtualSetting`) +Use this when value depends on other settings and should not be directly persisted. + +```csharp +MY_VIRTUAL_SETTING = new VirtualSetting( + typeof(bool), + value => { /* setter side-effects */ }, + () => { /* compute current value */ return true; } +).SetDependencies(DEP_A, DEP_B); +``` + +## Behavior Notes +- WhWz settings are loaded from JSON in `WhWzSettingManager`. +- If a WhWz stored value is invalid/unreadable, it is reset to default during load. +- Dolphin settings are read/written via `DolphinSettingManager` to `.ini` files. +- Setting changes publish through the settings signal bus (`ISettingsSignalBus`). + +## If You Add a New Value Type +- Update JSON parsing in `WheelWizard/Models/Settings/WhWzSetting.cs` (`SetFromJson`). +- Update INI parsing in `WheelWizard/Models/Settings/DolphinSetting.cs` (`SetFromString`) if needed. + +## Quick Checklist +- Register setting in `SettingsManager` constructor. +- Add public property/properties in `SettingsManager`. +- Add interface members in `ISettingsServices.cs`. +- Add validation. +- Use typed setting in calling code. From 684eb73b588a714cddf442189d103281fa11e13e Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:17:56 +0100 Subject: [PATCH 06/15] more changes --- .../DolphinInstaller/LinuxDolphinInstaller.cs | 5 +- .../DolphinInstaller/LinuxProcessService.cs | 2 +- .../Features/Settings/ADDING_SETTINGS.md | 102 ++++---- .../Features/Settings/ISettingsServices.cs | 23 -- .../Features/Settings/SettingsExtensions.cs | 4 +- .../Settings/SettingsLocalizationService.cs | 2 +- .../Features/Settings/SettingsManager.cs | 230 +++++++----------- WheelWizard/Features/Settings/TypedSetting.cs | 26 -- .../GameLicense/GameLicenseService.cs | 4 +- .../MiiManagement/MiiExtensions.cs | 2 +- .../Launcher/Helpers/DolphinLaunchHelper.cs | 2 +- .../Services/Launcher/RrBetaLauncher.cs | 2 +- WheelWizard/Services/Launcher/RrLauncher.cs | 2 +- WheelWizard/Services/PathManager.cs | 6 +- .../Services/WiiManagement/WiiMoteSettings.cs | 2 +- WheelWizard/Views/Layout.axaml.cs | 4 +- WheelWizard/Views/NavigationManager.cs | 2 +- WheelWizard/Views/Pages/FriendsPage.axaml.cs | 4 +- WheelWizard/Views/Pages/HomePage.axaml.cs | 4 +- WheelWizard/Views/Pages/MiiListPage.axaml.cs | 4 +- WheelWizard/Views/Pages/ModsPage.axaml.cs | 4 +- .../Views/Pages/RoomDetailsPage.axaml.cs | 2 +- .../Pages/Settings/OtherSettings.axaml.cs | 4 +- .../Views/Pages/UserProfilePage.axaml.cs | 2 +- 24 files changed, 166 insertions(+), 278 deletions(-) delete mode 100644 WheelWizard/Features/Settings/TypedSetting.cs diff --git a/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs index 8226bc61..af2f5725 100644 --- a/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs +++ b/WheelWizard/Features/DolphinInstaller/LinuxDolphinInstaller.cs @@ -71,9 +71,6 @@ public async Task InstallFlatpakDolphin(IProgress? progres return Fail($"Dolphin installation failed with exit code {installDolphinResult.Value}."); var launchResult = await processService.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4)); - if (launchResult.IsFailure) - return launchResult.Error; - - return Ok(); + return launchResult.IsFailure ? launchResult.Error : Ok(); } } diff --git a/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs b/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs index 97efcd12..f1c53112 100644 --- a/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs +++ b/WheelWizard/Features/DolphinInstaller/LinuxProcessService.cs @@ -103,7 +103,7 @@ private static void ReportProgress(string? output, IProgress? progress) return; var match = Regex.Match(output, @"(\d+)%"); - if (match.Success && int.TryParse(match.Groups[1].Value, out int percent)) + 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 index 4e076970..40956b39 100644 --- a/WheelWizard/Features/Settings/ADDING_SETTINGS.md +++ b/WheelWizard/Features/Settings/ADDING_SETTINGS.md @@ -1,99 +1,95 @@ # Adding a Setting in WheelWizard -This guide explains how to add a setting with the current Settings feature architecture. +This is the quick guide for adding settings with the current setup. -## Where Settings Live -- WheelWizard app settings (JSON): `WhWzSetting` -- Dolphin config settings (INI): `DolphinSetting` -- Derived/read-only computed settings: `VirtualSetting` +## Where to edit +1. `WheelWizard/Features/Settings/SettingsManager.cs` +2. `WheelWizard/Features/Settings/ISettingsServices.cs` -Main wiring is in `WheelWizard/Features/Settings/SettingsManager.cs`. +Note: you still touch 3 spots, but 2 are inside `SettingsManager.cs`: +1. Constructor registration +2. Public `Setting` property +3. Matching interface property -## Rules -- Add new setting logic in `Features/Settings`, not deprecated `Services/Helpers/Models/Utilities`. -- Use constructor-registered settings in `SettingsManager`. -- Use validation with `.SetValidation(...)` where possible. -- Prefer typed access (`ITypedSetting`) over raw `Setting` where possible. +## Setting Types +- WheelWizard JSON setting: `RegisterWhWz(...)` +- Dolphin INI setting: `RegisterDolphin(...)` +- Computed (not persisted): `VirtualSetting` -## 1) Add a new WhWz setting (stored in WheelWizard JSON) +## WhWz setting template +Use in `SettingsManager` constructor: -### Step A: Register in `SettingsManager` constructor ```csharp MY_NEW_SETTING = RegisterWhWz( - CreateWhWzSetting(typeof(bool), "MyNewSetting", false) - .SetValidation(value => value is bool) + "MyNewSetting", + false, + value => value is bool ); ``` -### Step B: Expose public properties in `SettingsManager` +Add property in `SettingsManager`: + ```csharp public Setting MY_NEW_SETTING { get; } -public ITypedSetting MyNewSetting { get; } ``` -And initialize typed wrapper in constructor: -```csharp -MyNewSetting = new TypedSetting(MY_NEW_SETTING); -``` +Add property in `IGeneralSettings` (or `IDolphinSettings` when appropriate): -### Step C: Add interface members in `ISettingsServices.cs` -Add raw + typed members to the correct interface (`IGeneralSettings` or `IDolphinSettings`): ```csharp Setting MY_NEW_SETTING { get; } -ITypedSetting MyNewSetting { get; } ``` -## 2) Add a new Dolphin setting (stored in Dolphin INI) +## Dolphin setting template +Use in `SettingsManager` constructor: -### Step A: Register in `SettingsManager` constructor ```csharp MY_DOLPHIN_SETTING = RegisterDolphin( - CreateDolphinSetting(typeof(int), ("GFX.ini", "Settings", "MyDolphinKey"), 0) - .SetValidation(value => (int)(value ?? -1) >= 0) + ("GFX.ini", "Settings", "MyDolphinKey"), + 0, + value => (int)(value ?? -1) >= 0 ); ``` -### Step B: Expose public property in `SettingsManager` +Add property in `SettingsManager`: + ```csharp public Setting MY_DOLPHIN_SETTING { get; } ``` -If you want typed access: -```csharp -public ITypedSetting MyDolphinSetting { get; } -MyDolphinSetting = new TypedSetting(MY_DOLPHIN_SETTING); -``` +Add property in `IDolphinSettings`: -### Step C: Add to interface in `ISettingsServices.cs` ```csharp Setting MY_DOLPHIN_SETTING { get; } -ITypedSetting MyDolphinSetting { get; } ``` -## 3) Add a derived/computed setting (`VirtualSetting`) -Use this when value depends on other settings and should not be directly persisted. +## Virtual setting template +Use when setting depends on other settings and should not be saved: ```csharp MY_VIRTUAL_SETTING = new VirtualSetting( typeof(bool), - value => { /* setter side-effects */ }, - () => { /* compute current value */ return true; } + value => { /* apply side-effects */ }, + () => { /* compute value */ return true; } ).SetDependencies(DEP_A, DEP_B); ``` -## Behavior Notes -- WhWz settings are loaded from JSON in `WhWzSettingManager`. -- If a WhWz stored value is invalid/unreadable, it is reset to default during load. -- Dolphin settings are read/written via `DolphinSettingManager` to `.ini` files. -- Setting changes publish through the settings signal bus (`ISettingsSignalBus`). +## Read/Write usage +Use type-safe manager methods in callers: + +```csharp +var value = settings.Get(settings.MY_NEW_SETTING); +settings.Set(settings.MY_NEW_SETTING, true); +``` -## If You Add a New Value Type -- Update JSON parsing in `WheelWizard/Models/Settings/WhWzSetting.cs` (`SetFromJson`). -- Update INI parsing in `WheelWizard/Models/Settings/DolphinSetting.cs` (`SetFromString`) if needed. +## Important notes +- No `ITypedSetting` layer anymore. +- WhWz invalid/unreadable values are reset to default during load. +- Setting change notifications go through `ISettingsSignalBus`. +- Keep new logic in `Features/Settings` (not deprecated folders). -## Quick Checklist -- Register setting in `SettingsManager` constructor. -- Add public property/properties in `SettingsManager`. -- Add interface members in `ISettingsServices.cs`. +## Minimal checklist +- Register setting in constructor. +- Add public `Setting` property. +- Add interface property. - Add validation. -- Use typed setting in calling code. +- Use `Get` / `Set(...)` where consumed. diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs index d16e046c..045d7aae 100644 --- a/WheelWizard/Features/Settings/ISettingsServices.cs +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -17,15 +17,6 @@ public interface IDolphinSettingManager void LoadSettings(); } -public interface ITypedSetting -{ - string Name { get; } - T Get(); - bool Set(T value, bool skipSave = false); - bool IsValid(); - Setting RawSetting { get; } -} - public interface IGeneralSettings { Setting USER_FOLDER_PATH { get; } @@ -41,19 +32,6 @@ public interface IGeneralSettings Setting REMOVE_BLUR { get; } Setting RR_REGION { get; } Setting WW_LANGUAGE { get; } - - ITypedSetting UserFolderPath { get; } - ITypedSetting DolphinLocation { get; } - ITypedSetting GameLocation { get; } - ITypedSetting ForceWiimote { get; } - ITypedSetting LaunchWithDolphin { get; } - ITypedSetting PrefersModsRowView { get; } - ITypedSetting FocusedUser { get; } - ITypedSetting EnableAnimations { get; } - ITypedSetting TestingModeEnabled { get; } - ITypedSetting SavedWindowScale { get; } - ITypedSetting RemoveBlur { get; } - ITypedSetting WwLanguage { get; } } public interface IDolphinSettings @@ -67,7 +45,6 @@ public interface IDolphinSettings Setting MACADDRESS { get; } Setting WINDOW_SCALE { get; } Setting RECOMMENDED_SETTINGS { get; } - ITypedSetting MacAddress { get; } } public interface ISettingsManager : IGeneralSettings, IDolphinSettings diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index d9221472..95dea673 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -5,8 +5,8 @@ public static class SettingsExtensions public static IServiceCollection AddSettings(this IServiceCollection services) { // TODO(naming-cleanup, later): - // - Prefix casing is inconsistent: `WhWz*` vs `WW_*` vs `Ww*` (example: `WhWzSettingManager`, `WW_LANGUAGE`, `WwLanguage`). - // - Some setting identifiers use all-caps while others use PascalCase (example: `MACADDRESS` vs `GAME_LOCATION` and typed props). + // - 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`). // Next step: diff --git a/WheelWizard/Features/Settings/SettingsLocalizationService.cs b/WheelWizard/Features/Settings/SettingsLocalizationService.cs index 1f8c1fae..39e73768 100644 --- a/WheelWizard/Features/Settings/SettingsLocalizationService.cs +++ b/WheelWizard/Features/Settings/SettingsLocalizationService.cs @@ -30,7 +30,7 @@ private void OnSignal(SettingChangedSignal signal) private void ApplyCulture() { - var newCulture = new CultureInfo(settingsManager.WwLanguage.Get()); + 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 index 035d0453..3e1e8c0b 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -37,142 +37,116 @@ IFileSystem fileSystem _fileSystem = fileSystem; DOLPHIN_LOCATION = RegisterWhWz( - CreateWhWzSetting(typeof(string), "DolphinLocation", "") - .SetValidation(value => - { - var pathOrCommand = value as string ?? string.Empty; - if (string.IsNullOrWhiteSpace(pathOrCommand)) - return false; + "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 (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - if ( - PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak() - ) - { - return false; - } - } - - return EnvHelper.IsValidUnixCommand(pathOrCommand); + if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak()) + return false; } - return _fileSystem.File.Exists(pathOrCommand); - }) + return EnvHelper.IsValidUnixCommand(pathOrCommand); + } + + return _fileSystem.File.Exists(pathOrCommand); + } ); USER_FOLDER_PATH = RegisterWhWz( - CreateWhWzSetting(typeof(string), "UserFolderPath", "") - .SetValidation(value => - { - var userFolderPath = value as string ?? string.Empty; - if (!_fileSystem.Directory.Exists(userFolderPath)) - return false; + "UserFolderPath", + "", + value => + { + var userFolderPath = value as string ?? string.Empty; + if (!_fileSystem.Directory.Exists(userFolderPath)) + return false; - string dolphinLocation = Get(DOLPHIN_LOCATION); + var dolphinLocation = Get(DOLPHIN_LOCATION); - // We cannot determine the validity of the user folder path in that case - if (string.IsNullOrWhiteSpace(dolphinLocation)) - return true; + // 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()) - { - // In this case, Dolphin would use `EMBEDDED_USER_DIR` (portable `user` directory). - if (_fileSystem.Directory.Exists("user")) - return false; + // 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; - // The Dolphin executable directory with `portable.txt` case - if (_fileSystem.File.Exists(Path.Combine(PathManager.GetDolphinExeDirectory(), "portable.txt"))) - return false; + // In this case, Dolphin would use `EMBEDDED_USER_DIR` (portable `user` directory). + if (_fileSystem.Directory.Exists("user")) + return false; - // The value of this environment variable would be used instead if it was somehow set - const string environmentVariableToAvoid = "DOLPHIN_EMU_USERPATH"; + // The Dolphin executable directory with `portable.txt` case + if (_fileSystem.File.Exists(Path.Combine(PathManager.GetDolphinExeDirectory(), "portable.txt"))) + return false; - if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(environmentVariableToAvoid))) - return false; + // The value of this environment variable would be used instead if it was somehow set + const string environmentVariableToAvoid = "DOLPHIN_EMU_USERPATH"; - if (dolphinLocation.Contains(environmentVariableToAvoid, StringComparison.Ordinal)) - return false; + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(environmentVariableToAvoid))) + return false; - // `~/.dolphin-emu` would be used if it exists - if ( - !PathManager.IsFlatpakDolphinFilePath() - && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath) - ) - return false; - } + if (dolphinLocation.Contains(environmentVariableToAvoid, StringComparison.Ordinal)) + return false; - return true; - }) - ); + // `~/.dolphin-emu` would be used if it exists + if (!PathManager.IsFlatpakDolphinFilePath() && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath)) + return false; - GAME_LOCATION = RegisterWhWz( - CreateWhWzSetting(typeof(string), "GameLocation", "") - .SetValidation(value => _fileSystem.File.Exists(value as string ?? string.Empty)) - ); - FORCE_WIIMOTE = RegisterWhWz(CreateWhWzSetting(typeof(bool), "ForceWiimote", false)); - LAUNCH_WITH_DOLPHIN = RegisterWhWz(CreateWhWzSetting(typeof(bool), "LaunchWithDolphin", false)); - PREFERS_MODS_ROW_VIEW = RegisterWhWz(CreateWhWzSetting(typeof(bool), "PrefersModsRowView", true)); - FOCUSED_USER = RegisterWhWz( - CreateWhWzSetting(typeof(int), "FavoriteUser", 0).SetValidation(value => (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4) + return true; + } ); - ENABLE_ANIMATIONS = RegisterWhWz(CreateWhWzSetting(typeof(bool), "EnableAnimations", true)); - TESTING_MODE_ENABLED = RegisterWhWz(CreateWhWzSetting(typeof(bool), "TestingModeEnabled", false)); - SAVED_WINDOW_SCALE = RegisterWhWz( - CreateWhWzSetting(typeof(double), "WindowScale", 1.0) - .SetValidation(value => (double)(value ?? -1) >= 0.5 && (double)(value ?? -1) <= 2.0) - ); - REMOVE_BLUR = RegisterWhWz(CreateWhWzSetting(typeof(bool), "REMOVE_BLUR", true)); - RR_REGION = RegisterWhWz(CreateWhWzSetting(typeof(MarioKartWiiEnums.Regions), "RR_Region", MarioKartWiiEnums.Regions.None)); - WW_LANGUAGE = RegisterWhWz( - CreateWhWzSetting(typeof(string), "WW_Language", "en") - .SetValidation(value => SettingValues.WhWzLanguages.ContainsKey((string)value!)) - ); + 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!)); NAND_ROOT_PATH = RegisterDolphin( - CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "NANDRootPath"), "") - .SetValidation(value => _fileSystem.Directory.Exists(value as string ?? string.Empty)) + ("Dolphin.ini", "General", "NANDRootPath"), + "", + value => _fileSystem.Directory.Exists(value as string ?? string.Empty) ); LOAD_PATH = RegisterDolphin( - CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "LoadPath"), "") - .SetValidation(value => _fileSystem.Directory.Exists(value as string ?? string.Empty)) + ("Dolphin.ini", "General", "LoadPath"), + "", + value => _fileSystem.Directory.Exists(value as string ?? string.Empty) ); - VSYNC = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Hardware", "VSync"), false)); - INTERNAL_RESOLUTION = RegisterDolphin( - CreateDolphinSetting(typeof(int), ("GFX.ini", "Settings", "InternalResolution"), 1) - .SetValidation(value => (int)(value ?? -1) >= 0) - ); - SHOW_FPS = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "ShowFPS"), false)); - GFX_BACKEND = RegisterDolphin( - CreateDolphinSetting(typeof(string), ("Dolphin.ini", "Core", "GFXBackend"), SettingValues.GFXRenderers.Values.First()) - ); + 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( - CreateDolphinSetting( - typeof(DolphinShaderCompilationMode), - ("GFX.ini", "Settings", "ShaderCompilationMode"), - DolphinShaderCompilationMode.Default - ) - ); - _dolphinCompileShadersAtStart = RegisterDolphin( - CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "WaitForShadersBeforeStarting"), false) - ); - _dolphinSsaa = RegisterDolphin(CreateDolphinSetting(typeof(bool), ("GFX.ini", "Settings", "SSAA"), false)); + _dolphinCompilationMode = RegisterDolphin(("GFX.ini", "Settings", "ShaderCompilationMode"), DolphinShaderCompilationMode.Default); + _dolphinCompileShadersAtStart = RegisterDolphin(("GFX.ini", "Settings", "WaitForShadersBeforeStarting"), false); + _dolphinSsaa = RegisterDolphin(("GFX.ini", "Settings", "SSAA"), false); _dolphinMsaa = RegisterDolphin( - CreateDolphinSetting(typeof(string), ("GFX.ini", "Settings", "MSAA"), "0x00000001") - .SetValidation(value => (value?.ToString() ?? "") is "0x00000001" or "0x00000002" or "0x00000004" or "0x00000008") + ("GFX.ini", "Settings", "MSAA"), + "0x00000001", + value => (value?.ToString() ?? "") is "0x00000001" or "0x00000002" or "0x00000004" or "0x00000008" ); // Readonly settings - MACADDRESS = RegisterDolphin(CreateDolphinSetting(typeof(string), ("Dolphin.ini", "General", "WirelessMac"), "02:01:02:03:04:05")); + MACADDRESS = RegisterDolphin(("Dolphin.ini", "General", "WirelessMac"), "02:01:02:03:04:05"); WINDOW_SCALE = new VirtualSetting( typeof(double), @@ -206,20 +180,6 @@ IFileSystem fileSystem return !value4 && value2 && value3 == "0x00000002" && value1 == DolphinShaderCompilationMode.HybridUberShaders; } ).SetDependencies(_dolphinCompilationMode, _dolphinCompileShadersAtStart, _dolphinMsaa, _dolphinSsaa); - - UserFolderPath = new TypedSetting(USER_FOLDER_PATH); - DolphinLocation = new TypedSetting(DOLPHIN_LOCATION); - GameLocation = new TypedSetting(GAME_LOCATION); - ForceWiimote = new TypedSetting(FORCE_WIIMOTE); - LaunchWithDolphin = new TypedSetting(LAUNCH_WITH_DOLPHIN); - PrefersModsRowView = new TypedSetting(PREFERS_MODS_ROW_VIEW); - FocusedUser = new TypedSetting(FOCUSED_USER); - EnableAnimations = new TypedSetting(ENABLE_ANIMATIONS); - TestingModeEnabled = new TypedSetting(TESTING_MODE_ENABLED); - SavedWindowScale = new TypedSetting(SAVED_WINDOW_SCALE); - RemoveBlur = new TypedSetting(REMOVE_BLUR); - WwLanguage = new TypedSetting(WW_LANGUAGE); - MacAddress = new TypedSetting(MACADDRESS); } public Setting USER_FOLDER_PATH { get; } @@ -246,20 +206,6 @@ IFileSystem fileSystem public Setting WINDOW_SCALE { get; } public Setting RECOMMENDED_SETTINGS { get; } - public ITypedSetting UserFolderPath { get; } - public ITypedSetting DolphinLocation { get; } - public ITypedSetting GameLocation { get; } - public ITypedSetting ForceWiimote { get; } - public ITypedSetting LaunchWithDolphin { get; } - public ITypedSetting PrefersModsRowView { get; } - public ITypedSetting FocusedUser { get; } - public ITypedSetting EnableAnimations { get; } - public ITypedSetting TestingModeEnabled { get; } - public ITypedSetting SavedWindowScale { get; } - public ITypedSetting RemoveBlur { get; } - public ITypedSetting WwLanguage { get; } - public ITypedSetting MacAddress { get; } - public T Get(Setting setting) { var value = setting.Get(); @@ -321,24 +267,22 @@ public void LoadSettings() } } - private WhWzSetting CreateWhWzSetting(Type valueType, string name, object defaultValue) + private WhWzSetting RegisterWhWz(string name, T defaultValue, Func? validation = null) { - return new(valueType, name, defaultValue, _whWzSettingManager.SaveSettings); - } + var setting = new WhWzSetting(typeof(T), name, defaultValue!, _whWzSettingManager.SaveSettings); + if (validation != null) + setting.SetValidation(validation); - private DolphinSetting CreateDolphinSetting(Type valueType, (string, string, string) location, object defaultValue) - { - return new(valueType, location, defaultValue, _dolphinSettingManager.SaveSettings); - } - - private Setting RegisterWhWz(WhWzSetting setting) - { _whWzSettingManager.RegisterSetting(setting); return setting; } - private Setting RegisterDolphin(DolphinSetting 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; } diff --git a/WheelWizard/Features/Settings/TypedSetting.cs b/WheelWizard/Features/Settings/TypedSetting.cs deleted file mode 100644 index b5e98bb8..00000000 --- a/WheelWizard/Features/Settings/TypedSetting.cs +++ /dev/null @@ -1,26 +0,0 @@ -using WheelWizard.Models.Settings; - -namespace WheelWizard.Settings; - -public sealed class TypedSetting : ITypedSetting -{ - public TypedSetting(Setting rawSetting) - { - RawSetting = rawSetting; - } - - public string Name => RawSetting.Name; - public Setting RawSetting { get; } - - public T Get() => (T)RawSetting.Get(); - - public bool IsValid() => RawSetting.IsValid(); - - public bool Set(T value, bool skipSave = false) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - return RawSetting.Set(value, skipSave); - } -} diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index ec7eff59..4dda96a1 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -134,9 +134,9 @@ ISettingsManager settingsManager /// /// Returns the "focused" or currently active license/user as determined by the Settings. /// - public LicenseProfile ActiveUser => Licenses.Users[_settingsManager.FocusedUser.Get()]; + public LicenseProfile ActiveUser => Licenses.Users[_settingsManager.Get(_settingsManager.FOCUSED_USER)]; - public List ActiveCurrentFriends => Licenses.Users[_settingsManager.FocusedUser.Get()].Friends; + public List ActiveCurrentFriends => Licenses.Users[_settingsManager.Get(_settingsManager.FOCUSED_USER)].Friends; public LicenseCollection LicenseCollection => Licenses; diff --git a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs index 73089a58..82e85d46 100644 --- a/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs +++ b/WheelWizard/Features/WiiManagement/MiiManagement/MiiExtensions.cs @@ -72,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 = Settings.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/Services/Launcher/Helpers/DolphinLaunchHelper.cs b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs index aab0fc7d..12dca25a 100644 --- a/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs +++ b/WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs @@ -132,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 = Settings.DolphinLocation.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 7771c68e..c7c0066d 100644 --- a/WheelWizard/Services/Launcher/RrBetaLauncher.cs +++ b/WheelWizard/Services/Launcher/RrBetaLauncher.cs @@ -46,7 +46,7 @@ public async Task Launch() } RetroRewindLaunchHelper.GenerateLaunchJson(PathManager.RrBetaXmlFilePath); - var dolphinLaunchType = _settingsManager.LaunchWithDolphin.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" ); diff --git a/WheelWizard/Services/Launcher/RrLauncher.cs b/WheelWizard/Services/Launcher/RrLauncher.cs index 81c4ad84..ec08959a 100644 --- a/WheelWizard/Services/Launcher/RrLauncher.cs +++ b/WheelWizard/Services/Launcher/RrLauncher.cs @@ -47,7 +47,7 @@ public async Task Launch() } RetroRewindLaunchHelper.GenerateLaunchJson(); - var dolphinLaunchType = _settingsManager.LaunchWithDolphin.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" ); diff --git a/WheelWizard/Services/PathManager.cs b/WheelWizard/Services/PathManager.cs index 9ee155a7..1719332b 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -36,9 +36,9 @@ static PathManager() public static string HomeFolderPath => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // Paths set by the user - public static string GameFilePath => Settings.GameLocation.Get(); - public static string DolphinFilePath => Settings.DolphinLocation.Get(); - public static string UserFolderPath => Settings.UserFolderPath.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); diff --git a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs index 91b9251d..0809c289 100644 --- a/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs +++ b/WheelWizard/Services/WiiManagement/WiiMoteSettings.cs @@ -14,7 +14,7 @@ public static class WiiMoteSettings public static void DisableVirtualWiiMote() => ModifyWiiMoteSource(0); - public static bool IsForceSettingsEnabled() => Settings.ForceWiimote.Get(); + public static bool IsForceSettingsEnabled() => Settings.Get(Settings.FORCE_WIIMOTE); private static string GetSavedWiiMoteLocation() { diff --git a/WheelWizard/Views/Layout.axaml.cs b/WheelWizard/Views/Layout.axaml.cs index 01ce965d..7d016108 100644 --- a/WheelWizard/Views/Layout.axaml.cs +++ b/WheelWizard/Views/Layout.axaml.cs @@ -238,7 +238,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve e.Handled = true; - if (SettingsService.TestingModeEnabled.Get()) + if (SettingsService.Get(SettingsService.TESTING_MODE_ENABLED)) return; if (_testerPromptOpen) @@ -280,7 +280,7 @@ private async void TitleLabel_OnPointerPressed(object? sender, PointerPressedEve private void UpdateTestingButtonVisibility() { - TestingButton.IsVisible = SettingsService.TestingModeEnabled.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 9ec702bf..066e831c 100644 --- a/WheelWizard/Views/NavigationManager.cs +++ b/WheelWizard/Views/NavigationManager.cs @@ -15,7 +15,7 @@ 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 = Settings.WwLanguage.Get(); + var itsSupposeToBe = Settings.Get(Settings.WW_LANGUAGE); if (itCurrentlyIs != itsSupposeToBe) { Settings.Set(Settings.WW_LANGUAGE, itCurrentlyIs); diff --git a/WheelWizard/Views/Pages/FriendsPage.axaml.cs b/WheelWizard/Views/Pages/FriendsPage.axaml.cs index ff36a776..4a3afe4a 100644 --- a/WheelWizard/Views/Pages/FriendsPage.axaml.cs +++ b/WheelWizard/Views/Pages/FriendsPage.axaml.cs @@ -143,7 +143,7 @@ private void SortByDropdown_OnSelectionChanged(object? sender, SelectionChangedE private async void AddFriend_OnClick(object? sender, RoutedEventArgs e) { - var focusedUserIndex = SettingsService.FocusedUser.Get(); + var focusedUserIndex = SettingsService.Get(SettingsService.FOCUSED_USER); if (focusedUserIndex is < 0 or > 3) { ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); @@ -318,7 +318,7 @@ private void RemoveFriend_OnClick(object sender, RoutedEventArgs e) if (string.IsNullOrWhiteSpace(selectedPlayer.FriendCode)) return; - var focusedUserIndex = SettingsService.FocusedUser.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 e14dcfa6..ccf8386c 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -193,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 (!SettingsService.EnableAnimations.Get()) + if (!SettingsService.Get(SettingsService.ENABLE_ANIMATIONS)) return; var allowedToRun = WaitForWheelTrailState( @@ -224,7 +224,7 @@ private async void PlayEntranceAnimation() private async void PlayActivateAnimation() { - if (!SettingsService.EnableAnimations.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 b3c84cdf..1b81890d 100644 --- a/WheelWizard/Views/Pages/MiiListPage.axaml.cs +++ b/WheelWizard/Views/Pages/MiiListPage.axaml.cs @@ -421,7 +421,7 @@ private async void CreateNewMii() if (!save) return; - var result = MiiDbService.AddToDatabase(window.Mii, SettingsService.MacAddress.Get()); + var result = MiiDbService.AddToDatabase(window.Mii, SettingsService.Get(SettingsService.MACADDRESS)); if (result.IsFailure) { ViewUtils.ShowSnackbar( @@ -437,7 +437,7 @@ private async void CreateNewMii() private void DuplicateMii(Mii[] miis) { //assuming the mac address is already set correctly - var macAddress = SettingsService.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 0f1ffd59..629c9865 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml.cs +++ b/WheelWizard/Views/Pages/ModsPage.axaml.cs @@ -183,7 +183,7 @@ private void ButtonDown_OnClick(object? sender, RoutedEventArgs e) private void ToggleModsPageView_OnClick(object? sender, RoutedEventArgs e) { - var current = SettingsService.PrefersModsRowView.Get(); + var current = SettingsService.Get(SettingsService.PREFERS_MODS_ROW_VIEW); SettingsService.Set(SettingsService.PREFERS_MODS_ROW_VIEW, !current); SetModsViewVariant(); } @@ -191,7 +191,7 @@ private void ToggleModsPageView_OnClick(object? sender, RoutedEventArgs e) private void SetModsViewVariant() { Control[] elementsToSwapClasses = [ToggleButton, ModsListBox]; - var asRows = SettingsService.PrefersModsRowView.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 54283f1b..f5ec7339 100644 --- a/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs +++ b/WheelWizard/Views/Pages/RoomDetailsPage.axaml.cs @@ -135,7 +135,7 @@ private async void AddFriend_OnClick(object sender, RoutedEventArgs e) return; } - var focusedUserIndex = SettingsService.FocusedUser.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/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index 4f058dea..a0b62449 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -41,8 +41,8 @@ public OtherSettings() private void LoadSettings() { // Only loads when the settings are not disabled (aka when the paths are set up correctly) - DisableForce.IsChecked = SettingsService.ForceWiimote.Get(); - LaunchWithDolphin.IsChecked = SettingsService.LaunchWithDolphin.Get(); + DisableForce.IsChecked = SettingsService.Get(SettingsService.FORCE_WIIMOTE); + LaunchWithDolphin.IsChecked = SettingsService.Get(SettingsService.LAUNCH_WITH_DOLPHIN); OpenSaveFolderButton.IsEnabled = Directory.Exists(PathManager.SaveFolderPath); } diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs index d484fb5c..7b1309db 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs @@ -113,7 +113,7 @@ public int ActiveInfoSlideIndex } private int _currentUserIndex; - private int FocusedUser => SettingsService.FocusedUser.Get(); + private int FocusedUser => SettingsService.Get(SettingsService.FOCUSED_USER); public UserProfilePage() { From c465f62fcc69ad347559901c4a514b3e28aaeb88 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:42:41 +0100 Subject: [PATCH 07/15] move settings files --- .../Settings/DolphinSettingManager.cs | 2 +- .../Settings/Domain}/DolphinSetting.cs | 2 +- .../Settings/Domain}/Setting.cs | 23 +------ .../Settings/Domain}/SettingConstants.cs | 2 +- .../Settings/Domain}/VirtualSetting.cs | 2 +- .../Settings/Domain}/WhWzSetting.cs | 2 +- .../Features/Settings/ISettingsServices.cs | 2 +- .../Features/Settings/SettingsManager.cs | 16 ++++- .../Features/Settings/SettingsRuntime.cs | 66 ++++++++++++++++++- .../Features/Settings/SettingsSignalBus.cs | 56 +--------------- .../Features/Settings/WhWzSettingManager.cs | 2 +- .../GameLicense/GameLicenseService.cs | 2 +- WheelWizard/Models/{Settings => Mods}/Mod.cs | 2 +- .../Models/Settings/ISettingListener.cs | 6 -- WheelWizard/Models/Settings/ListedSetting.cs | 22 ------- .../Services/Installation/ModInstallation.cs | 2 +- WheelWizard/Services/ModManager.cs | 2 +- .../CurrentUserProfile.axaml.cs | 2 +- WheelWizard/Views/Layout.axaml.cs | 22 +++++-- WheelWizard/Views/Pages/ModsPage.axaml.cs | 2 +- .../Views/Pages/Settings/AppInfo.axaml.cs | 1 - .../Pages/Settings/OtherSettings.axaml.cs | 5 -- .../Pages/Settings/SettingsPage.axaml.cs | 4 -- .../Pages/Settings/VideoSettings.axaml.cs | 4 +- .../Pages/Settings/WhWzSettings.axaml.cs | 5 +- .../Views/Pages/UserProfilePage.axaml.cs | 2 +- 26 files changed, 114 insertions(+), 144 deletions(-) rename WheelWizard/{Models/Settings => Features/Settings/Domain}/DolphinSetting.cs (98%) rename WheelWizard/{Models/Settings => Features/Settings/Domain}/Setting.cs (65%) rename WheelWizard/{Models/Settings => Features/Settings/Domain}/SettingConstants.cs (97%) rename WheelWizard/{Models/Settings => Features/Settings/Domain}/VirtualSetting.cs (98%) rename WheelWizard/{Models/Settings => Features/Settings/Domain}/WhWzSetting.cs (98%) rename WheelWizard/Models/{Settings => Mods}/Mod.cs (98%) delete mode 100644 WheelWizard/Models/Settings/ISettingListener.cs delete mode 100644 WheelWizard/Models/Settings/ListedSetting.cs diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 1667cce9..5cee9dfb 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; -using WheelWizard.Models.Settings; using WheelWizard.Services; +using WheelWizard.Settings.Domain; namespace WheelWizard.Settings; diff --git a/WheelWizard/Models/Settings/DolphinSetting.cs b/WheelWizard/Features/Settings/Domain/DolphinSetting.cs similarity index 98% rename from WheelWizard/Models/Settings/DolphinSetting.cs rename to WheelWizard/Features/Settings/Domain/DolphinSetting.cs index 0db1b17f..aab8eebc 100644 --- a/WheelWizard/Models/Settings/DolphinSetting.cs +++ b/WheelWizard/Features/Settings/Domain/DolphinSetting.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Domain; public class DolphinSetting : Setting { diff --git a/WheelWizard/Models/Settings/Setting.cs b/WheelWizard/Features/Settings/Domain/Setting.cs similarity index 65% rename from WheelWizard/Models/Settings/Setting.cs rename to WheelWizard/Features/Settings/Domain/Setting.cs index 21d5d9ec..0ce6efb6 100644 --- a/WheelWizard/Models/Settings/Setting.cs +++ b/WheelWizard/Features/Settings/Domain/Setting.cs @@ -1,6 +1,6 @@ using WheelWizard.Settings; -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Domain; public abstract class Setting { @@ -12,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; } @@ -61,23 +60,5 @@ public Setting SetForceSave(bool saveEvenIfNotValid) return this; } - [Obsolete("Use ISettingsSignalBus subscriptions instead of direct setting subscriptions.")] - public bool Unsubscribe(ISettingListener dependent) => DependentVirtualSettings.Remove(dependent); - - [Obsolete("Use ISettingsSignalBus subscriptions instead of direct setting subscriptions.")] - public void Subscribe(ISettingListener dependent) - { - if (!DependentVirtualSettings.Contains(dependent)) - DependentVirtualSettings.Add(dependent); - } - - protected void SignalChange() - { - SettingsSignalRuntime.Publish(this); - - 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/Domain/SettingConstants.cs similarity index 97% rename from WheelWizard/Models/Settings/SettingConstants.cs rename to WheelWizard/Features/Settings/Domain/SettingConstants.cs index bd1de544..fc856940 100644 --- a/WheelWizard/Models/Settings/SettingConstants.cs +++ b/WheelWizard/Features/Settings/Domain/SettingConstants.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Domain; public enum DolphinShaderCompilationMode { diff --git a/WheelWizard/Models/Settings/VirtualSetting.cs b/WheelWizard/Features/Settings/Domain/VirtualSetting.cs similarity index 98% rename from WheelWizard/Models/Settings/VirtualSetting.cs rename to WheelWizard/Features/Settings/Domain/VirtualSetting.cs index fb0c2d57..46d35114 100644 --- a/WheelWizard/Models/Settings/VirtualSetting.cs +++ b/WheelWizard/Features/Settings/Domain/VirtualSetting.cs @@ -1,6 +1,6 @@ using WheelWizard.Settings; -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Domain; public class VirtualSetting : Setting { diff --git a/WheelWizard/Models/Settings/WhWzSetting.cs b/WheelWizard/Features/Settings/Domain/WhWzSetting.cs similarity index 98% rename from WheelWizard/Models/Settings/WhWzSetting.cs rename to WheelWizard/Features/Settings/Domain/WhWzSetting.cs index f47eeb61..73acf676 100644 --- a/WheelWizard/Models/Settings/WhWzSetting.cs +++ b/WheelWizard/Features/Settings/Domain/WhWzSetting.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace WheelWizard.Models.Settings; +namespace WheelWizard.Settings.Domain; public class WhWzSetting : Setting { diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs index 045d7aae..7fda3c10 100644 --- a/WheelWizard/Features/Settings/ISettingsServices.cs +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -1,4 +1,4 @@ -using WheelWizard.Models.Settings; +using WheelWizard.Settings.Domain; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 3e1e8c0b..1d1ebe96 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -3,8 +3,8 @@ using WheelWizard.DolphinInstaller; using WheelWizard.Helpers; using WheelWizard.Models.Enums; -using WheelWizard.Models.Settings; using WheelWizard.Services; +using WheelWizard.Settings.Domain; namespace WheelWizard.Settings; @@ -24,6 +24,7 @@ public class SettingsManager : ISettingsManager private bool _hasLoadedSettings; private double _internalScale = -1.0; + #region Constructor public SettingsManager( IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager, @@ -36,6 +37,7 @@ IFileSystem fileSystem _linuxDolphinInstaller = linuxDolphinInstaller; _fileSystem = fileSystem; + #region WhWz settings DOLPHIN_LOCATION = RegisterWhWz( "DolphinLocation", "", @@ -117,7 +119,9 @@ IFileSystem fileSystem 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"), "", @@ -147,7 +151,9 @@ IFileSystem fileSystem // 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!, @@ -180,8 +186,11 @@ IFileSystem fileSystem 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; } @@ -205,7 +214,9 @@ IFileSystem fileSystem 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(); @@ -266,7 +277,9 @@ public void 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); @@ -286,4 +299,5 @@ private DolphinSetting RegisterDolphin((string, string, string) location, T d _dolphinSettingManager.RegisterSetting(setting); return setting; } + #endregion } diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs index da766ea3..c86ad100 100644 --- a/WheelWizard/Features/Settings/SettingsRuntime.cs +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -1,8 +1,13 @@ +using WheelWizard.Settings.Domain; + namespace WheelWizard.Settings; -// TODO: This file is temporary. -// it serves as a bridge for legacy code that cannot get constructor injection (like PathManager or NaviagtionManager). -// Once those are all one day migrated or removed, we can and MUST remove this Runtime +// 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 readonly object SyncRoot = new(); @@ -27,3 +32,58 @@ public static void Initialize(ISettingsManager settingsManager) } } } + +[Obsolete("SettingsSignalRuntime is deprecated. Use constructor injection for ISettingsSignalBus instead.")] +public static class SettingsSignalRuntime +{ + private static readonly object SyncRoot = new(); + private static ISettingsSignalBus? _current; + private static readonly List> PendingInitializers = []; + + public static void Initialize(ISettingsSignalBus signalBus) + { + ArgumentNullException.ThrowIfNull(signalBus); + + List> callbacksToRun; + lock (SyncRoot) + { + _current = signalBus; + callbacksToRun = [.. PendingInitializers]; + PendingInitializers.Clear(); + } + + foreach (var callback in callbacksToRun) + { + callback(signalBus); + } + } + + public static void OnInitialized(Action callback) + { + ArgumentNullException.ThrowIfNull(callback); + + ISettingsSignalBus? signalBus; + lock (SyncRoot) + { + signalBus = _current; + if (signalBus == null) + { + PendingInitializers.Add(callback); + return; + } + } + + callback(signalBus); + } + + public static void Publish(Setting setting) + { + ISettingsSignalBus? signalBus; + lock (SyncRoot) + { + signalBus = _current; + } + + signalBus?.Publish(setting); + } +} diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs index d4de388a..eeae5797 100644 --- a/WheelWizard/Features/Settings/SettingsSignalBus.cs +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -1,4 +1,4 @@ -using WheelWizard.Models.Settings; +using WheelWizard.Settings.Domain; namespace WheelWizard.Settings; @@ -68,57 +68,3 @@ public void Dispose() } } } - -public static class SettingsSignalRuntime -{ - private static readonly object SyncRoot = new(); - private static ISettingsSignalBus? _current; - private static readonly List> PendingInitializers = []; - - public static void Initialize(ISettingsSignalBus signalBus) - { - ArgumentNullException.ThrowIfNull(signalBus); - - List> callbacksToRun; - lock (SyncRoot) - { - _current = signalBus; - callbacksToRun = [.. PendingInitializers]; - PendingInitializers.Clear(); - } - - foreach (var callback in callbacksToRun) - { - callback(signalBus); - } - } - - public static void OnInitialized(Action callback) - { - ArgumentNullException.ThrowIfNull(callback); - - ISettingsSignalBus? signalBus; - lock (SyncRoot) - { - signalBus = _current; - if (signalBus == null) - { - PendingInitializers.Add(callback); - return; - } - } - - callback(signalBus); - } - - public static void Publish(Setting setting) - { - ISettingsSignalBus? signalBus; - lock (SyncRoot) - { - signalBus = _current; - } - - signalBus?.Publish(setting); - } -} diff --git a/WheelWizard/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs index b0775999..38327fe7 100644 --- a/WheelWizard/Features/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -1,8 +1,8 @@ using System.IO.Abstractions; using System.Text.Json; using Microsoft.Extensions.Logging; -using WheelWizard.Models.Settings; using WheelWizard.Services; +using WheelWizard.Settings.Domain; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index 4dda96a1..812e7689 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.Settings; +using WheelWizard.Settings.Domain; using WheelWizard.Utilities.Generators; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.WheelWizardData; 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/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/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/Views/BehaviorComponent/CurrentUserProfile.axaml.cs b/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs index 5682e6b1..72a0e869 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.Domain; 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 7d016108..6f63bab1 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.Settings; +using WheelWizard.Settings.Domain; 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!; @@ -49,6 +50,9 @@ public partial class Layout : BaseWindow, IRepeatedTaskListener, ISettingListene [Inject] private ISettingsManager SettingsService { get; set; } = null!; + [Inject] + private ISettingsSignalBus SettingsSignalBus { get; set; } = null!; + public Layout() { Instance = this; @@ -56,8 +60,7 @@ public Layout() AddLayer(); OnSettingChanged(SettingsService.SAVED_WINDOW_SCALE); - SettingsService.WINDOW_SCALE.Subscribe(this); - SettingsService.TESTING_MODE_ENABLED.Subscribe(this); + _settingsSignalSubscription = SettingsSignalBus.Subscribe(OnSettingSignal); UpdateTestingButtonVisibility(); var completeString = Humanizer.ReplaceDynamic(Phrases.Text_MadeByString, "Patchzy", "WantToBeeMe"); @@ -94,7 +97,16 @@ 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 == SettingsService.WINDOW_SCALE || setting == SettingsService.SAVED_WINDOW_SCALE) diff --git a/WheelWizard/Views/Pages/ModsPage.axaml.cs b/WheelWizard/Views/Pages/ModsPage.axaml.cs index 629c9865..beca7905 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml.cs +++ b/WheelWizard/Views/Pages/ModsPage.axaml.cs @@ -3,7 +3,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using WheelWizard.Models.Settings; +using WheelWizard.Models.Mods; using WheelWizard.Services; using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; diff --git a/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs b/WheelWizard/Views/Pages/Settings/AppInfo.axaml.cs index b1751f89..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; diff --git a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index a0b62449..8eb6eea4 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -1,11 +1,6 @@ -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.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Generic; 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 c840f87f..3b8659cb 100644 --- a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs @@ -1,11 +1,9 @@ using Avalonia.Controls; using Avalonia.Interactivity; -using WheelWizard.Models.Settings; using WheelWizard.Settings; +using WheelWizard.Settings.Domain; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; -using WheelWizard.Views; -using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Views.Pages.Settings; diff --git a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs index 9d0221e0..523367bf 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs @@ -1,21 +1,18 @@ -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.Settings; +using WheelWizard.Settings.Domain; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; -using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; using Button = WheelWizard.Views.Components.Button; using SettingsResource = WheelWizard.Resources.Languages.Settings; diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs index 7b1309db..6d34f591 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.Settings; +using WheelWizard.Settings.Domain; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Components; From 858f18d5a69318254310fab18c2502f1d6b8ef51 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:52:39 +0100 Subject: [PATCH 08/15] guard clauses --- .../Features/Settings/SettingsManager.cs | 10 +++++----- .../Features/Settings/SettingsSignalBus.cs | 3 +-- .../Settings/SettingsStartupInitializer.cs | 20 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 1d1ebe96..6f01b2b4 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -49,11 +49,11 @@ IFileSystem fileSystem if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak()) - return false; - } + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return EnvHelper.IsValidUnixCommand(pathOrCommand); + + if (PathManager.IsFlatpakDolphinFilePath(pathOrCommand) && !_linuxDolphinInstaller.IsDolphinInstalledInFlatpak()) + return false; return EnvHelper.IsValidUnixCommand(pathOrCommand); } diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs index eeae5797..756059d7 100644 --- a/WheelWizard/Features/Settings/SettingsSignalBus.cs +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -18,8 +18,7 @@ public sealed class SettingsSignalBus : ISettingsSignalBus public IDisposable Subscribe(Action handler) { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + ArgumentNullException.ThrowIfNull(handler); long id; lock (_syncRoot) diff --git a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs index 3b7e1c5d..b07757aa 100644 --- a/WheelWizard/Features/Settings/SettingsStartupInitializer.cs +++ b/WheelWizard/Features/Settings/SettingsStartupInitializer.cs @@ -24,17 +24,17 @@ public void Initialize() } var report = reportResult.Value; - if (!report.IsValid) + if (report.IsValid) + return; + + foreach (var issue in report.Issues) { - foreach (var issue in report.Issues) - { - logger.LogWarning( - "Settings validation warning: {Code} ({SettingName}) {Message}", - issue.Code, - issue.SettingName, - issue.Message - ); - } + logger.LogWarning( + "Settings validation warning: {Code} ({SettingName}) {Message}", + issue.Code, + issue.SettingName, + issue.Message + ); } } } From 67e728058886c1ef84f4146eb1d9069d2df67f91 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:14:10 +0100 Subject: [PATCH 09/15] add tests for the settings --- .../Features/Settings/DolphinSettingsTests.cs | 143 +++++++++ .../Features/Settings/SettingsTests.cs | 299 ++++++++++++++++++ .../Features/Settings/VirtualSettingsTests.cs | 57 ++++ .../Features/Settings/WhWzSettingsTests.cs | 159 ++++++++++ .../Features/Settings/ADDING_SETTINGS.md | 89 ++---- .../Features/Settings/ISettingsServices.cs | 8 +- .../Settings => Services}/ModConfigManager.cs | 0 7 files changed, 686 insertions(+), 69 deletions(-) create mode 100644 WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs create mode 100644 WheelWizard.Test/Features/Settings/SettingsTests.cs create mode 100644 WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs create mode 100644 WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs rename WheelWizard/{Features/Settings => Services}/ModConfigManager.cs (100%) diff --git a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs new file mode 100644 index 00000000..434b2429 --- /dev/null +++ b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs @@ -0,0 +1,143 @@ +using Testably.Abstractions.Testing; +using WheelWizard.Services; +using WheelWizard.Settings; +using WheelWizard.Settings.Domain; + +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 +{ + [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())); + } +} diff --git a/WheelWizard.Test/Features/Settings/SettingsTests.cs b/WheelWizard.Test/Features/Settings/SettingsTests.cs new file mode 100644 index 00000000..d0043d56 --- /dev/null +++ b/WheelWizard.Test/Features/Settings/SettingsTests.cs @@ -0,0 +1,299 @@ +using System.Globalization; +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; +using Testably.Abstractions; +using Testably.Abstractions.Testing; +using WheelWizard.DolphinInstaller; +using WheelWizard.Settings; +using WheelWizard.Settings.Domain; + +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 = new SettingsSignalBus(); + 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 = new SettingsSignalBus(); + 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 = new SettingsSignalBus(); + + 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 = new SettingsSignalBus(); + 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 = new SettingsSignalBus(); + 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 = new SettingsSignalBus(); + 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 = new SettingsSignalBus(); + 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 ?? new SettingsSignalBus()); +#pragma warning restore CS0618 + } + + 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; + } +} diff --git a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs new file mode 100644 index 00000000..7f172d9b --- /dev/null +++ b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs @@ -0,0 +1,57 @@ +using WheelWizard.Settings; +using WheelWizard.Settings.Domain; + +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(new SettingsSignalBus()); + 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..4520e281 --- /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.Domain; + +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/Settings/ADDING_SETTINGS.md b/WheelWizard/Features/Settings/ADDING_SETTINGS.md index 40956b39..bcb43ea3 100644 --- a/WheelWizard/Features/Settings/ADDING_SETTINGS.md +++ b/WheelWizard/Features/Settings/ADDING_SETTINGS.md @@ -1,47 +1,32 @@ # Adding a Setting in WheelWizard -This is the quick guide for adding settings with the current setup. - -## Where to edit -1. `WheelWizard/Features/Settings/SettingsManager.cs` -2. `WheelWizard/Features/Settings/ISettingsServices.cs` - -Note: you still touch 3 spots, but 2 are inside `SettingsManager.cs`: -1. Constructor registration -2. Public `Setting` property -3. Matching interface property - ## Setting Types -- WheelWizard JSON setting: `RegisterWhWz(...)` -- Dolphin INI setting: `RegisterDolphin(...)` -- Computed (not persisted): `VirtualSetting` - -## WhWz setting template -Use in `SettingsManager` constructor: +- **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 -MY_NEW_SETTING = RegisterWhWz( - "MyNewSetting", - false, - value => value is bool -); +Setting MY_NEW_SETTING { get; } ``` - -Add property in `SettingsManager`: - +then you also define this setting in the `SettingsManager.cs` as a property ```csharp public Setting MY_NEW_SETTING { get; } ``` -Add property in `IGeneralSettings` (or `IDolphinSettings` when appropriate): +after that you have to register the setting. This depends on the type of setting you want to add. +### Wheel Wizard ```csharp -Setting MY_NEW_SETTING { get; } +MY_NEW_SETTING = RegisterWhWz( + "MyNewSetting", + false, + value => value is bool +); ``` -## Dolphin setting template -Use in `SettingsManager` constructor: - +### Dolphin ```csharp MY_DOLPHIN_SETTING = RegisterDolphin( ("GFX.ini", "Settings", "MyDolphinKey"), @@ -50,46 +35,24 @@ MY_DOLPHIN_SETTING = RegisterDolphin( ); ``` -Add property in `SettingsManager`: - -```csharp -public Setting MY_DOLPHIN_SETTING { get; } -``` - -Add property in `IDolphinSettings`: - -```csharp -Setting MY_DOLPHIN_SETTING { get; } -``` - -## Virtual setting template -Use when setting depends on other settings and should not be saved: - +### Virtual ```csharp MY_VIRTUAL_SETTING = new VirtualSetting( typeof(bool), value => { /* apply side-effects */ }, () => { /* compute value */ return true; } -).SetDependencies(DEP_A, DEP_B); +).SetDependencies(SETTING_A, SETTING_B); ``` +Usually you create virtual settigns that reference one or more real settings. +The value of the virtual setting is cached. However, if the value relies on e.g. SETTING_A. than once SETTING_A changes, your cache is wrong. +// For that reason, you have to set dependencies. That way if SETTING_A changes, the virtual setting gets a signal to recompute its value -## Read/Write usage -Use type-safe manager methods in callers: +## Reading/Writing settings +Use type-safe manager methods in callers: ```csharp -var value = settings.Get(settings.MY_NEW_SETTING); -settings.Set(settings.MY_NEW_SETTING, true); +// reading +bool value = SettingsManager.Get(SettingsManager.MY_NEW_SETTING); +// writeing +SettingsManager.Set(SettingsManager.MY_NEW_SETTING, true); ``` - -## Important notes -- No `ITypedSetting` layer anymore. -- WhWz invalid/unreadable values are reset to default during load. -- Setting change notifications go through `ISettingsSignalBus`. -- Keep new logic in `Features/Settings` (not deprecated folders). - -## Minimal checklist -- Register setting in constructor. -- Add public `Setting` property. -- Add interface property. -- Add validation. -- Use `Get` / `Set(...)` where consumed. diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs index 7fda3c10..be972593 100644 --- a/WheelWizard/Features/Settings/ISettingsServices.cs +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -17,7 +17,7 @@ public interface IDolphinSettingManager void LoadSettings(); } -public interface IGeneralSettings +public interface ISettingsProperties { Setting USER_FOLDER_PATH { get; } Setting DOLPHIN_LOCATION { get; } @@ -32,10 +32,6 @@ public interface IGeneralSettings Setting REMOVE_BLUR { get; } Setting RR_REGION { get; } Setting WW_LANGUAGE { get; } -} - -public interface IDolphinSettings -{ Setting NAND_ROOT_PATH { get; } Setting LOAD_PATH { get; } Setting VSYNC { get; } @@ -47,7 +43,7 @@ public interface IDolphinSettings Setting RECOMMENDED_SETTINGS { get; } } -public interface ISettingsManager : IGeneralSettings, IDolphinSettings +public interface ISettingsManager : ISettingsProperties { OperationResult ValidateCorePathSettings(); diff --git a/WheelWizard/Features/Settings/ModConfigManager.cs b/WheelWizard/Services/ModConfigManager.cs similarity index 100% rename from WheelWizard/Features/Settings/ModConfigManager.cs rename to WheelWizard/Services/ModConfigManager.cs From 48dfde804f92c2d768ac5a024a24667f11595d1d Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:40:39 +0100 Subject: [PATCH 10/15] Update SettingsExtensions.cs --- WheelWizard/Features/Settings/SettingsExtensions.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index 95dea673..239de32a 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -4,19 +4,12 @@ public static class SettingsExtensions { public static IServiceCollection AddSettings(this IServiceCollection services) { - // TODO(naming-cleanup, later): + // 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`). - // Next step: - // - we created Settings Singal bus, make sure it is also being used everywhere such that you dont subseribve or unsubscribe to a setting model anymore. - // - Remove/ move the model to the settings add it in Domains. make sure there is no ugly remenance somewhere in the depricated codebase anymore - // - i saw setting still use path manager, can we simply replace that iwt the IFileSystem? or does that need some more work - // - investigate if localization service really has to be its own setting service or if it can be combined with the rest of the settings. OR maybe even better, that it can become its own localization feature (that uses setting manager) - // - look at the settings initializer, and if we really need that or if there is a better way - - // look at all the code as high level overview + // TODO: Investigate / migrate to IOptions: https://learn.microsoft.com/en-us/dotnet/core/extensions/options services.AddSingleton(); services.AddSingleton(); From 6379e40698aa7d6bbe01466bea976b0e9deefd17 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:44:58 +0100 Subject: [PATCH 11/15] rename to types --- WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs | 2 +- WheelWizard.Test/Features/Settings/SettingsTests.cs | 2 +- WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs | 2 +- WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs | 2 +- WheelWizard/Features/Settings/DolphinSettingManager.cs | 2 +- WheelWizard/Features/Settings/ISettingsServices.cs | 2 +- WheelWizard/Features/Settings/SettingsExtensions.cs | 2 +- WheelWizard/Features/Settings/SettingsManager.cs | 2 +- WheelWizard/Features/Settings/SettingsRuntime.cs | 2 +- WheelWizard/Features/Settings/SettingsSignalBus.cs | 2 +- .../Features/Settings/{Domain => Types}/DolphinSetting.cs | 2 +- WheelWizard/Features/Settings/{Domain => Types}/Setting.cs | 2 +- .../Features/Settings/{Domain => Types}/SettingConstants.cs | 2 +- .../Features/Settings/{Domain => Types}/VirtualSetting.cs | 2 +- WheelWizard/Features/Settings/{Domain => Types}/WhWzSetting.cs | 2 +- WheelWizard/Features/Settings/WhWzSettingManager.cs | 2 +- .../Features/WiiManagement/GameLicense/GameLicenseService.cs | 2 +- WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs | 2 +- WheelWizard/Views/Layout.axaml.cs | 2 +- WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs | 2 +- WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs | 2 +- WheelWizard/Views/Pages/UserProfilePage.axaml.cs | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) rename WheelWizard/Features/Settings/{Domain => Types}/DolphinSetting.cs (98%) rename WheelWizard/Features/Settings/{Domain => Types}/Setting.cs (97%) rename WheelWizard/Features/Settings/{Domain => Types}/SettingConstants.cs (97%) rename WheelWizard/Features/Settings/{Domain => Types}/VirtualSetting.cs (98%) rename WheelWizard/Features/Settings/{Domain => Types}/WhWzSetting.cs (98%) diff --git a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs index 434b2429..883eefce 100644 --- a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs @@ -1,7 +1,7 @@ using Testably.Abstractions.Testing; using WheelWizard.Services; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Test.Features.Settings; diff --git a/WheelWizard.Test/Features/Settings/SettingsTests.cs b/WheelWizard.Test/Features/Settings/SettingsTests.cs index d0043d56..907fdff1 100644 --- a/WheelWizard.Test/Features/Settings/SettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/SettingsTests.cs @@ -5,7 +5,7 @@ using Testably.Abstractions.Testing; using WheelWizard.DolphinInstaller; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Test.Features.Settings; diff --git a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs index 7f172d9b..5ca6d6de 100644 --- a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs @@ -1,5 +1,5 @@ using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Test.Features.Settings; diff --git a/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs b/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs index 4520e281..9a82b942 100644 --- a/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/WhWzSettingsTests.cs @@ -3,7 +3,7 @@ using Testably.Abstractions.Testing; using WheelWizard.Services; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Test.Features.Settings; diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 5cee9dfb..4ead727e 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; using WheelWizard.Services; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/ISettingsServices.cs b/WheelWizard/Features/Settings/ISettingsServices.cs index be972593..063518a3 100644 --- a/WheelWizard/Features/Settings/ISettingsServices.cs +++ b/WheelWizard/Features/Settings/ISettingsServices.cs @@ -1,4 +1,4 @@ -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/SettingsExtensions.cs b/WheelWizard/Features/Settings/SettingsExtensions.cs index 239de32a..eb8e8a0b 100644 --- a/WheelWizard/Features/Settings/SettingsExtensions.cs +++ b/WheelWizard/Features/Settings/SettingsExtensions.cs @@ -9,7 +9,7 @@ public static IServiceCollection AddSettings(this IServiceCollection services) // - 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 + // TODO: Investigate / migrate to IOptions: https://learn.microsoft.com/en-us/dotnet/core/extensions/options services.AddSingleton(); services.AddSingleton(); diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index 6f01b2b4..d24ee0f4 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -4,7 +4,7 @@ using WheelWizard.Helpers; using WheelWizard.Models.Enums; using WheelWizard.Services; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs index c86ad100..b61f69f6 100644 --- a/WheelWizard/Features/Settings/SettingsRuntime.cs +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -1,4 +1,4 @@ -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs index 756059d7..3c426868 100644 --- a/WheelWizard/Features/Settings/SettingsSignalBus.cs +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -1,4 +1,4 @@ -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/Settings/Domain/DolphinSetting.cs b/WheelWizard/Features/Settings/Types/DolphinSetting.cs similarity index 98% rename from WheelWizard/Features/Settings/Domain/DolphinSetting.cs rename to WheelWizard/Features/Settings/Types/DolphinSetting.cs index aab8eebc..65c222d3 100644 --- a/WheelWizard/Features/Settings/Domain/DolphinSetting.cs +++ b/WheelWizard/Features/Settings/Types/DolphinSetting.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.Settings.Domain; +namespace WheelWizard.Settings.Types; public class DolphinSetting : Setting { diff --git a/WheelWizard/Features/Settings/Domain/Setting.cs b/WheelWizard/Features/Settings/Types/Setting.cs similarity index 97% rename from WheelWizard/Features/Settings/Domain/Setting.cs rename to WheelWizard/Features/Settings/Types/Setting.cs index 0ce6efb6..09c58f88 100644 --- a/WheelWizard/Features/Settings/Domain/Setting.cs +++ b/WheelWizard/Features/Settings/Types/Setting.cs @@ -1,6 +1,6 @@ using WheelWizard.Settings; -namespace WheelWizard.Settings.Domain; +namespace WheelWizard.Settings.Types; public abstract class Setting { diff --git a/WheelWizard/Features/Settings/Domain/SettingConstants.cs b/WheelWizard/Features/Settings/Types/SettingConstants.cs similarity index 97% rename from WheelWizard/Features/Settings/Domain/SettingConstants.cs rename to WheelWizard/Features/Settings/Types/SettingConstants.cs index fc856940..73508520 100644 --- a/WheelWizard/Features/Settings/Domain/SettingConstants.cs +++ b/WheelWizard/Features/Settings/Types/SettingConstants.cs @@ -1,4 +1,4 @@ -namespace WheelWizard.Settings.Domain; +namespace WheelWizard.Settings.Types; public enum DolphinShaderCompilationMode { diff --git a/WheelWizard/Features/Settings/Domain/VirtualSetting.cs b/WheelWizard/Features/Settings/Types/VirtualSetting.cs similarity index 98% rename from WheelWizard/Features/Settings/Domain/VirtualSetting.cs rename to WheelWizard/Features/Settings/Types/VirtualSetting.cs index 46d35114..770ab8ae 100644 --- a/WheelWizard/Features/Settings/Domain/VirtualSetting.cs +++ b/WheelWizard/Features/Settings/Types/VirtualSetting.cs @@ -1,6 +1,6 @@ using WheelWizard.Settings; -namespace WheelWizard.Settings.Domain; +namespace WheelWizard.Settings.Types; public class VirtualSetting : Setting { diff --git a/WheelWizard/Features/Settings/Domain/WhWzSetting.cs b/WheelWizard/Features/Settings/Types/WhWzSetting.cs similarity index 98% rename from WheelWizard/Features/Settings/Domain/WhWzSetting.cs rename to WheelWizard/Features/Settings/Types/WhWzSetting.cs index 73acf676..23a3786d 100644 --- a/WheelWizard/Features/Settings/Domain/WhWzSetting.cs +++ b/WheelWizard/Features/Settings/Types/WhWzSetting.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace WheelWizard.Settings.Domain; +namespace WheelWizard.Settings.Types; public class WhWzSetting : Setting { diff --git a/WheelWizard/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs index 38327fe7..e9bd6524 100644 --- a/WheelWizard/Features/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using WheelWizard.Services; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; namespace WheelWizard.Settings; diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index 812e7689..9bcbfb25 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -7,7 +7,7 @@ using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; using WheelWizard.Utilities.Generators; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.WheelWizardData; diff --git a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs b/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs index 72a0e869..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.Resources.Languages; -using WheelWizard.Settings.Domain; +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 6f63bab1..822be5b7 100644 --- a/WheelWizard/Views/Layout.axaml.cs +++ b/WheelWizard/Views/Layout.axaml.cs @@ -10,7 +10,7 @@ using WheelWizard.Resources.Languages; using WheelWizard.Services.LiveData; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.Views.Components; diff --git a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs index 3b8659cb..37ed1df3 100644 --- a/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/VideoSettings.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Interactivity; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; diff --git a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs index 523367bf..b3a87ad4 100644 --- a/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs @@ -10,7 +10,7 @@ using WheelWizard.Resources.Languages; using WheelWizard.Services; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Popups.Generic; diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs index 6d34f591..ac6e1525 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml.cs +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml.cs @@ -8,7 +8,7 @@ using WheelWizard.Services.LiveData; using WheelWizard.Services.Other; using WheelWizard.Settings; -using WheelWizard.Settings.Domain; +using WheelWizard.Settings.Types; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Components; From 90d222b7c1a03a455c19244ac0847212fb9b075f Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:06:50 +0100 Subject: [PATCH 12/15] fix spelling mistakes --- WheelWizard/Features/Settings/ADDING_SETTINGS.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WheelWizard/Features/Settings/ADDING_SETTINGS.md b/WheelWizard/Features/Settings/ADDING_SETTINGS.md index bcb43ea3..8f296e55 100644 --- a/WheelWizard/Features/Settings/ADDING_SETTINGS.md +++ b/WheelWizard/Features/Settings/ADDING_SETTINGS.md @@ -43,10 +43,9 @@ MY_VIRTUAL_SETTING = new VirtualSetting( () => { /* compute value */ return true; } ).SetDependencies(SETTING_A, SETTING_B); ``` -Usually you create virtual settigns that reference one or more real settings. -The value of the virtual setting is cached. However, if the value relies on e.g. SETTING_A. than once SETTING_A changes, your cache is wrong. -// For that reason, you have to set dependencies. That way if SETTING_A changes, the virtual setting gets a signal to recompute its value - +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: From fabc666c9a3e27075dd1f0feccd19370cf8eba72 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:55:53 +0100 Subject: [PATCH 13/15] fix tests --- .../Features/LinuxDolphinInstallerTests.cs | 10 +++++++ .../Features/Settings/DolphinSettingsTests.cs | 8 ++++- .../Features/Settings/SettingsTests.cs | 29 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs index 972586a9..8cef2545 100644 --- a/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs +++ b/WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs @@ -26,6 +26,16 @@ public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakInfoExitCodeIsZer 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() { diff --git a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs index 883eefce..d1647f84 100644 --- a/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/DolphinSettingsTests.cs @@ -56,7 +56,7 @@ public void SetFromString_Throws_WhenTypeIsUnsupported() } [Collection("SettingsFeature")] -public class DolphinSettingManagerTests +public class DolphinSettingManagerTests : IDisposable { [Fact] public void LoadSettings_ReadsExistingValue_FromIniFile() @@ -140,4 +140,10 @@ public void ReloadSettings_ReReadsFile_AfterItChangesOnDisk() 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 index 907fdff1..0ef9532d 100644 --- a/WheelWizard.Test/Features/Settings/SettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/SettingsTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.IO.Abstractions; +using System.Reflection; using Microsoft.Extensions.Logging; using Testably.Abstractions; using Testably.Abstractions.Testing; @@ -264,6 +265,26 @@ public static void InitializeSignalRuntime(ISettingsSignalBus? signalBus = null) #pragma warning restore CS0618 } + 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()) @@ -296,4 +317,12 @@ private static ISettingsManager CreateRuntimeSettingsStub(string userFolderPath, 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); + } } From 9a86aeef301ea789c4ee1201c1c60fb8d4a69d48 Mon Sep 17 00:00:00 2001 From: WantToBeeMe <93130991+WantToBeeMe@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:32:49 +0100 Subject: [PATCH 14/15] removed lock overkill --- .../Features/Settings/SettingsTests.cs | 22 +++++---- .../Features/Settings/VirtualSettingsTests.cs | 2 +- .../Settings/DolphinSettingManager.cs | 15 +++++-- .../Settings/SettingsLocalizationService.cs | 14 +++--- .../Features/Settings/SettingsManager.cs | 14 +++--- .../Features/Settings/SettingsRuntime.cs | 45 +++++-------------- .../Features/Settings/SettingsSignalBus.cs | 31 +++++++++---- .../Features/Settings/Types/DolphinSetting.cs | 2 +- .../Features/Settings/Types/WhWzSetting.cs | 2 +- .../Features/Settings/WhWzSettingManager.cs | 7 +++ 10 files changed, 78 insertions(+), 76 deletions(-) diff --git a/WheelWizard.Test/Features/Settings/SettingsTests.cs b/WheelWizard.Test/Features/Settings/SettingsTests.cs index 0ef9532d..3b974b37 100644 --- a/WheelWizard.Test/Features/Settings/SettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/SettingsTests.cs @@ -114,7 +114,7 @@ public class SettingsSignalBusTests [Fact] public void Publish_NotifiesActiveSubscribers() { - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var setting = new WhWzSetting(typeof(int), "Volume", 10); SettingChangedSignal? receivedSignal = null; using var _ = signalBus.Subscribe(signal => receivedSignal = signal); @@ -128,7 +128,7 @@ public void Publish_NotifiesActiveSubscribers() [Fact] public void DisposeSubscription_StopsReceivingSignals() { - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var setting = new WhWzSetting(typeof(int), "Volume", 10); var receiveCount = 0; var subscription = signalBus.Subscribe(_ => receiveCount++); @@ -143,7 +143,7 @@ public void DisposeSubscription_StopsReceivingSignals() [Fact] public void Subscribe_Throws_WhenHandlerIsNull() { - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); Assert.Throws(() => signalBus.Subscribe(null!)); } @@ -157,7 +157,7 @@ public void Initialize_SetsCurrentCulture_FromLanguageSetting() { var originalCulture = CultureInfo.CurrentCulture; var originalUiCulture = CultureInfo.CurrentUICulture; - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var settingsManager = Substitute.For(); var languageSetting = new WhWzSetting(typeof(string), "WW_Language", "fr"); settingsManager.WW_LANGUAGE.Returns(languageSetting); @@ -183,7 +183,7 @@ public void PublishLanguageSignal_UpdatesCulture_WhenLanguageChanges() { var originalCulture = CultureInfo.CurrentCulture; var originalUiCulture = CultureInfo.CurrentUICulture; - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var settingsManager = Substitute.For(); var languageSetting = new WhWzSetting(typeof(string), "WW_Language", "en"); settingsManager.WW_LANGUAGE.Returns(languageSetting); @@ -214,7 +214,7 @@ public class SettingsStartupInitializerTests public void Initialize_LoadsSettings_InitializesLocalization_AndSetsRuntimes() { var settingsManager = Substitute.For(); - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var localizationService = Substitute.For(); var logger = Substitute.For>(); settingsManager.ValidateCorePathSettings().Returns(Ok(new SettingsValidationReport([]))); @@ -233,7 +233,7 @@ public void Initialize_LoadsSettings_InitializesLocalization_AndSetsRuntimes() public void Initialize_DoesNotThrow_WhenValidationFails() { var settingsManager = Substitute.For(); - var signalBus = new SettingsSignalBus(); + var signalBus = SettingsTestUtils.CreateSettingsSignalBus(); var localizationService = Substitute.For(); var logger = Substitute.For>(); settingsManager.ValidateCorePathSettings().Returns(Fail("validation failed")); @@ -261,10 +261,16 @@ public static ISettingsManager InitializeSettingsRuntime(string userFolderPath, public static void InitializeSignalRuntime(ISettingsSignalBus? signalBus = null) { #pragma warning disable CS0618 - SettingsSignalRuntime.Initialize(signalBus ?? new SettingsSignalBus()); + 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 diff --git a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs index 5ca6d6de..c9c77c8c 100644 --- a/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs +++ b/WheelWizard.Test/Features/Settings/VirtualSettingsTests.cs @@ -37,7 +37,7 @@ public void Set_ReturnsFalseAndKeepsOldValue_WhenValidationFails() [Fact] public void SetDependencies_RecalculatesValue_WhenDependencySignalsChange() { - SettingsTestUtils.InitializeSignalRuntime(new SettingsSignalBus()); + SettingsTestUtils.InitializeSignalRuntime(SettingsTestUtils.CreateSettingsSignalBus()); var dependency = new WhWzSetting(typeof(int), "Dependency", 1); var setting = new VirtualSetting(typeof(int), _ => { }, () => (int)dependency.Get()).SetDependencies(dependency); diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 4ead727e..9918e9d3 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -8,6 +8,13 @@ public class DolphinSettingManager(IFileSystem fileSystem) : IDolphinSettingMana { private static string ConfigFolderPath(string fileName) => Path.Combine(PathManager.ConfigFolderPath, fileName); + // 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; @@ -60,8 +67,13 @@ public void ReloadSettings() 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 where waiting + // for the lock to open if (_loaded) return; @@ -69,9 +81,6 @@ public void LoadSettings() settingsSnapshot = [.. _settings]; } - if (!fileSystem.Directory.Exists(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. lock (_fileIoSync) diff --git a/WheelWizard/Features/Settings/SettingsLocalizationService.cs b/WheelWizard/Features/Settings/SettingsLocalizationService.cs index 39e73768..5017e00a 100644 --- a/WheelWizard/Features/Settings/SettingsLocalizationService.cs +++ b/WheelWizard/Features/Settings/SettingsLocalizationService.cs @@ -5,21 +5,17 @@ namespace WheelWizard.Settings; public sealed class SettingsLocalizationService(ISettingsManager settingsManager, ISettingsSignalBus settingsSignalBus) : ISettingsLocalizationService { - private readonly object _syncRoot = new(); private bool _initialized; private IDisposable? _subscription; public void Initialize() { - lock (_syncRoot) - { - if (_initialized) - return; + if (_initialized) + return; - _subscription = settingsSignalBus.Subscribe(OnSignal); - ApplyCulture(); - _initialized = true; - } + _subscription = settingsSignalBus.Subscribe(OnSignal); + ApplyCulture(); + _initialized = true; } private void OnSignal(SettingChangedSignal signal) diff --git a/WheelWizard/Features/Settings/SettingsManager.cs b/WheelWizard/Features/Settings/SettingsManager.cs index d24ee0f4..d9ae7adf 100644 --- a/WheelWizard/Features/Settings/SettingsManager.cs +++ b/WheelWizard/Features/Settings/SettingsManager.cs @@ -10,7 +10,6 @@ namespace WheelWizard.Settings; public class SettingsManager : ISettingsManager { - private readonly object _syncRoot = new(); private readonly IWhWzSettingManager _whWzSettingManager; private readonly IDolphinSettingManager _dolphinSettingManager; private readonly ILinuxDolphinInstaller _linuxDolphinInstaller; @@ -267,15 +266,12 @@ public OperationResult ValidateCorePathSettings() public void LoadSettings() { - lock (_syncRoot) - { - if (_hasLoadedSettings) - return; + if (_hasLoadedSettings) + return; - _whWzSettingManager.LoadSettings(); - _dolphinSettingManager.LoadSettings(); - _hasLoadedSettings = true; - } + _whWzSettingManager.LoadSettings(); + _dolphinSettingManager.LoadSettings(); + _hasLoadedSettings = true; } #endregion diff --git a/WheelWizard/Features/Settings/SettingsRuntime.cs b/WheelWizard/Features/Settings/SettingsRuntime.cs index b61f69f6..2692494c 100644 --- a/WheelWizard/Features/Settings/SettingsRuntime.cs +++ b/WheelWizard/Features/Settings/SettingsRuntime.cs @@ -10,33 +10,22 @@ namespace WheelWizard.Settings; [Obsolete("SettingsRuntime is deprecated. Use constructor injection for ISettingsManager instead.")] public static class SettingsRuntime { - private static readonly object SyncRoot = new(); private static ISettingsManager? _current; public static ISettingsManager Current { - get - { - lock (SyncRoot) - { - return _current ?? throw new InvalidOperationException("Settings runtime has not been initialized yet."); - } - } + get { return _current ?? throw new InvalidOperationException("Settings runtime has not been initialized yet."); } } public static void Initialize(ISettingsManager settingsManager) { - lock (SyncRoot) - { - _current = settingsManager; - } + _current = settingsManager; } } [Obsolete("SettingsSignalRuntime is deprecated. Use constructor injection for ISettingsSignalBus instead.")] public static class SettingsSignalRuntime { - private static readonly object SyncRoot = new(); private static ISettingsSignalBus? _current; private static readonly List> PendingInitializers = []; @@ -44,13 +33,9 @@ public static void Initialize(ISettingsSignalBus signalBus) { ArgumentNullException.ThrowIfNull(signalBus); - List> callbacksToRun; - lock (SyncRoot) - { - _current = signalBus; - callbacksToRun = [.. PendingInitializers]; - PendingInitializers.Clear(); - } + _current = signalBus; + var callbacksToRun = PendingInitializers.ToArray(); + PendingInitializers.Clear(); foreach (var callback in callbacksToRun) { @@ -62,15 +47,11 @@ public static void OnInitialized(Action callback) { ArgumentNullException.ThrowIfNull(callback); - ISettingsSignalBus? signalBus; - lock (SyncRoot) + var signalBus = _current; + if (signalBus == null) { - signalBus = _current; - if (signalBus == null) - { - PendingInitializers.Add(callback); - return; - } + PendingInitializers.Add(callback); + return; } callback(signalBus); @@ -78,12 +59,6 @@ public static void OnInitialized(Action callback) public static void Publish(Setting setting) { - ISettingsSignalBus? signalBus; - lock (SyncRoot) - { - signalBus = _current; - } - - signalBus?.Publish(setting); + _current?.Publish(setting); } } diff --git a/WheelWizard/Features/Settings/SettingsSignalBus.cs b/WheelWizard/Features/Settings/SettingsSignalBus.cs index 3c426868..460001ec 100644 --- a/WheelWizard/Features/Settings/SettingsSignalBus.cs +++ b/WheelWizard/Features/Settings/SettingsSignalBus.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using WheelWizard.Settings.Types; namespace WheelWizard.Settings; @@ -10,9 +11,13 @@ public interface ISettingsSignalBus void Publish(Setting setting); } -public sealed class SettingsSignalBus : ISettingsSignalBus +public sealed class SettingsSignalBus(ILogger logger) : ISettingsSignalBus { - private readonly object _syncRoot = new(); + // 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; @@ -21,7 +26,7 @@ public IDisposable Subscribe(Action handler) ArgumentNullException.ThrowIfNull(handler); long id; - lock (_syncRoot) + lock (_syncSubscribers) { id = _nextSubscriberId++; _subscribers[id] = handler; @@ -33,21 +38,29 @@ public IDisposable Subscribe(Action handler) public void Publish(Setting setting) { Action[] handlers; - lock (_syncRoot) - { - handlers = [.. _subscribers.Values]; - } + + // 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) { - handler(signal); + 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 (_syncRoot) + lock (_syncSubscribers) { _subscribers.Remove(subscriberId); } diff --git a/WheelWizard/Features/Settings/Types/DolphinSetting.cs b/WheelWizard/Features/Settings/Types/DolphinSetting.cs index 65c222d3..4bc67365 100644 --- a/WheelWizard/Features/Settings/Types/DolphinSetting.cs +++ b/WheelWizard/Features/Settings/Types/DolphinSetting.cs @@ -13,7 +13,7 @@ public DolphinSetting(Type type, (string, string, string) location, object defau public DolphinSetting(Type type, (string, string, string) location, object defaultValue, Action saveAction) : base(type, location.Item3, defaultValue) { - _saveAction = saveAction; + _saveAction = saveAction ?? throw new ArgumentNullException(nameof(saveAction)); FileName = location.Item1; Section = location.Item2; // name/key = location.Item3 diff --git a/WheelWizard/Features/Settings/Types/WhWzSetting.cs b/WheelWizard/Features/Settings/Types/WhWzSetting.cs index 23a3786d..43a4c582 100644 --- a/WheelWizard/Features/Settings/Types/WhWzSetting.cs +++ b/WheelWizard/Features/Settings/Types/WhWzSetting.cs @@ -12,7 +12,7 @@ public WhWzSetting(Type type, string name, object defaultValue) public WhWzSetting(Type type, string name, object defaultValue, Action saveAction) : base(type, name, defaultValue) { - _saveAction = saveAction; + _saveAction = saveAction ?? throw new ArgumentNullException(nameof(saveAction)); } protected override bool SetInternal(object newValue, bool skipSave = false) diff --git a/WheelWizard/Features/Settings/WhWzSettingManager.cs b/WheelWizard/Features/Settings/WhWzSettingManager.cs index e9bd6524..4a7accdc 100644 --- a/WheelWizard/Features/Settings/WhWzSettingManager.cs +++ b/WheelWizard/Features/Settings/WhWzSettingManager.cs @@ -8,6 +8,13 @@ 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; From 77e4cc56e248f10e4638c9a229ec5f2a40433241 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:59:18 +0100 Subject: [PATCH 15/15] use filesystem + update comment --- WheelWizard/Features/Settings/DolphinSettingManager.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/WheelWizard/Features/Settings/DolphinSettingManager.cs b/WheelWizard/Features/Settings/DolphinSettingManager.cs index 9918e9d3..0a1f7090 100644 --- a/WheelWizard/Features/Settings/DolphinSettingManager.cs +++ b/WheelWizard/Features/Settings/DolphinSettingManager.cs @@ -6,11 +6,12 @@ namespace WheelWizard.Settings; public class DolphinSettingManager(IFileSystem fileSystem) : IDolphinSettingManager { - private static string ConfigFolderPath(string fileName) => Path.Combine(PathManager.ConfigFolderPath, fileName); + private string ConfigFolderPath(string fileName) => fileSystem.Path.Combine(PathManager.ConfigFolderPath, fileName); // 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. + // 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 @@ -72,7 +73,7 @@ public void LoadSettings() lock (_syncRoot) { - // Since we are working with concurrency here, we have to check loaded again since it might be changed while we where waiting + // 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;