From 104ab1ace4abe17e4c34092b0ba5e8e3794c8926 Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:13:22 +0100 Subject: [PATCH 01/50] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bc78471d..b6d74e93 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,4 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp +AGENTS.md 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 02/50] 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 5b99f898601b07dc670c20135f4e8f846747c9d9 Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:29:19 +0100 Subject: [PATCH 03/50] Add leaderboard page and podium card component --- .../RrRooms/Domain/RwfcLeaderboardEntry.cs | 2 +- WheelWizard/Views/App.axaml | 3 +- .../WhWzLibrary/LeaderboardPodiumCard.axaml | 468 +++++++++++++++++ .../LeaderboardPodiumCard.axaml.cs | 100 ++++ WheelWizard/Views/Layout.axaml | 3 + WheelWizard/Views/Pages/LeaderboardPage.axaml | 247 +++++++++ .../Views/Pages/LeaderboardPage.axaml.cs | 485 ++++++++++++++++++ .../Views/Pages/LeaderboardPlayerItem.cs | 26 + 8 files changed, 1332 insertions(+), 2 deletions(-) create mode 100644 WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml create mode 100644 WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs create mode 100644 WheelWizard/Views/Pages/LeaderboardPage.axaml create mode 100644 WheelWizard/Views/Pages/LeaderboardPage.axaml.cs create mode 100644 WheelWizard/Views/Pages/LeaderboardPlayerItem.cs diff --git a/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs b/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs index c72a4fcf..dae9630a 100644 --- a/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs +++ b/WheelWizard/Features/RrRooms/Domain/RwfcLeaderboardEntry.cs @@ -16,5 +16,5 @@ public sealed class RwfcLeaderboardEntry public RwfcLeaderboardVrStats? VrStats { get; set; } - public string? MiiImageBase64 { get; set; } + public string? MiiData { get; set; } } diff --git a/WheelWizard/Views/App.axaml b/WheelWizard/Views/App.axaml index 65f76ef6..f7afb145 100644 --- a/WheelWizard/Views/App.axaml +++ b/WheelWizard/Views/App.axaml @@ -70,5 +70,6 @@ + - \ No newline at end of file + diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml new file mode 100644 index 00000000..64104b03 --- /dev/null +++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs new file mode 100644 index 00000000..d07c1096 --- /dev/null +++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml.cs @@ -0,0 +1,100 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using WheelWizard.WheelWizardData.Domain; +using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; + +namespace WheelWizard.Views.Components; + +public class LeaderboardPodiumCard : TemplatedControl +{ + public static readonly StyledProperty RankProperty = AvaloniaProperty.Register(nameof(Rank)); + + public int Rank + { + get => GetValue(RankProperty); + set => SetValue(RankProperty, value); + } + + public static readonly StyledProperty PlacementLabelProperty = AvaloniaProperty.Register( + nameof(PlacementLabel), + string.Empty + ); + + public string PlacementLabel + { + get => GetValue(PlacementLabelProperty); + set => SetValue(PlacementLabelProperty, value); + } + + public static readonly StyledProperty PlayerNameProperty = AvaloniaProperty.Register( + nameof(PlayerName), + string.Empty + ); + + public string PlayerName + { + get => GetValue(PlayerNameProperty); + set => SetValue(PlayerNameProperty, value); + } + + public static readonly StyledProperty VrTextProperty = AvaloniaProperty.Register( + nameof(VrText), + "--" + ); + + public string VrText + { + get => GetValue(VrTextProperty); + set => SetValue(VrTextProperty, value); + } + + public static readonly StyledProperty MiiProperty = AvaloniaProperty.Register(nameof(Mii)); + + public Mii? Mii + { + get => GetValue(MiiProperty); + set => SetValue(MiiProperty, value); + } + + public static readonly StyledProperty AvatarSizeProperty = AvaloniaProperty.Register( + nameof(AvatarSize), + 104 + ); + + public double AvatarSize + { + get => GetValue(AvatarSizeProperty); + set => SetValue(AvatarSizeProperty, value); + } + + public static readonly StyledProperty BadgeVariantProperty = AvaloniaProperty.Register< + LeaderboardPodiumCard, + BadgeVariant + >(nameof(BadgeVariant), BadgeVariant.None); + + public BadgeVariant BadgeVariant + { + get => GetValue(BadgeVariantProperty); + set => SetValue(BadgeVariantProperty, value); + } + + public static readonly StyledProperty ShowBadgeProperty = AvaloniaProperty.Register( + nameof(ShowBadge) + ); + + public bool ShowBadge + { + get => GetValue(ShowBadgeProperty); + set => SetValue(ShowBadgeProperty, value); + } + + public static readonly StyledProperty IsSuspiciousProperty = AvaloniaProperty.Register( + nameof(IsSuspicious) + ); + + public bool IsSuspicious + { + get => GetValue(IsSuspiciousProperty); + set => SetValue(IsSuspiciousProperty, value); + } +} diff --git a/WheelWizard/Views/Layout.axaml b/WheelWizard/Views/Layout.axaml index e0512497..c91c38ee 100644 --- a/WheelWizard/Views/Layout.axaml +++ b/WheelWizard/Views/Layout.axaml @@ -134,6 +134,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs new file mode 100644 index 00000000..f7b9b363 --- /dev/null +++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml.cs @@ -0,0 +1,485 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Interactivity; +using WheelWizard.Models; +using WheelWizard.Resources.Languages; +using WheelWizard.RrRooms; +using WheelWizard.Services.Settings; +using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Utilities.Generators; +using WheelWizard.Views.Popups; +using WheelWizard.Views.Popups.MiiManagement; +using WheelWizard.WheelWizardData; +using WheelWizard.WheelWizardData.Domain; +using WheelWizard.WiiManagement.GameLicense; +using WheelWizard.WiiManagement.MiiManagement; +using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; + +namespace WheelWizard.Views.Pages; + +public partial class LeaderboardPage : UserControlBase, INotifyPropertyChanged +{ + private CancellationTokenSource? _loadCts; + + [Inject] + private IRrLeaderboardSingletonService LeaderboardService { get; set; } = null!; + + [Inject] + private IWhWzDataSingletonService BadgeService { get; set; } = null!; + + [Inject] + private IGameLicenseSingletonService GameDataService { get; set; } = null!; + + private bool _hasLoadedOnce; + private bool _hasError; + private bool _hasNoData; + private bool _hasData; + private string _errorMessage = string.Empty; + private int _loadedPlayerCount; + private LeaderboardPlayerItem? _podiumFirst; + private LeaderboardPlayerItem? _podiumSecond; + private LeaderboardPlayerItem? _podiumThird; + + public ObservableCollection RemainingPlayers { get; } = []; + + public bool HasError + { + get => _hasError; + private set + { + if (_hasError == value) + return; + _hasError = value; + OnPropertyChanged(nameof(HasError)); + } + } + + public bool HasNoData + { + get => _hasNoData; + private set + { + if (_hasNoData == value) + return; + _hasNoData = value; + OnPropertyChanged(nameof(HasNoData)); + } + } + + public bool HasData + { + get => _hasData; + private set + { + if (_hasData == value) + return; + _hasData = value; + OnPropertyChanged(nameof(HasData)); + } + } + + public string ErrorMessage + { + get => _errorMessage; + private set + { + if (_errorMessage == value) + return; + _errorMessage = value; + OnPropertyChanged(nameof(ErrorMessage)); + } + } + + public string TotalPlayerCountText => _loadedPlayerCount.ToString(); + + public string RemainingCountText => $"{RemainingPlayers.Count} players"; + + public LeaderboardPlayerItem? PodiumFirst + { + get => _podiumFirst; + private set + { + if (_podiumFirst == value) + return; + _podiumFirst = value; + OnPropertyChanged(nameof(PodiumFirst)); + OnPropertyChanged(nameof(HasPodiumFirst)); + } + } + + public LeaderboardPlayerItem? PodiumSecond + { + get => _podiumSecond; + private set + { + if (_podiumSecond == value) + return; + _podiumSecond = value; + OnPropertyChanged(nameof(PodiumSecond)); + OnPropertyChanged(nameof(HasPodiumSecond)); + } + } + + public LeaderboardPlayerItem? PodiumThird + { + get => _podiumThird; + private set + { + if (_podiumThird == value) + return; + _podiumThird = value; + OnPropertyChanged(nameof(PodiumThird)); + OnPropertyChanged(nameof(HasPodiumThird)); + } + } + + public bool HasPodiumFirst => PodiumFirst != null; + public bool HasPodiumSecond => PodiumSecond != null; + public bool HasPodiumThird => PodiumThird != null; + + public LeaderboardPage() + { + InitializeComponent(); + DataContext = this; + RemainingPlayers.CollectionChanged += RemainingPlayers_OnCollectionChanged; + + Loaded += LeaderboardPage_Loaded; + Unloaded += LeaderboardPage_Unloaded; + } + + private async void LeaderboardPage_Loaded(object? sender, RoutedEventArgs e) + { + if (_hasLoadedOnce) + return; + + _hasLoadedOnce = true; + await ReloadLeaderboardAsync(); + } + + private void LeaderboardPage_Unloaded(object? sender, RoutedEventArgs e) + { + CancelCurrentLoad(); + } + + private void RemainingPlayers_OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(RemainingCountText)); + } + + private async void RetryButton_OnClick(object? sender, RoutedEventArgs e) + { + await ReloadLeaderboardAsync(); + } + + private async Task ReloadLeaderboardAsync() + { + CancelCurrentLoad(); + _loadCts = new(); + var cancellationToken = _loadCts.Token; + + SetLoadingState(); + ClearLeaderboardData(); + + var leaderboardResult = await LeaderboardService.GetTopPlayersAsync(50); + if (cancellationToken.IsCancellationRequested) + return; + + if (leaderboardResult.IsFailure) + { + SetErrorState(leaderboardResult.Error?.Message ?? "Unable to fetch leaderboard."); + return; + } + + var orderedEntries = leaderboardResult + .Value.Select((entry, index) => new { Entry = entry, Rank = ResolveRank(entry, index) }) + .OrderBy(entry => entry.Rank) + .Take(50) + .ToList(); + + if (orderedEntries.Count == 0) + { + SetEmptyState(); + return; + } + + var mappedPlayers = orderedEntries.Select((entry, index) => CreateLeaderboardPlayer(entry.Entry, entry.Rank, index)).ToList(); + + _loadedPlayerCount = mappedPlayers.Count; + OnPropertyChanged(nameof(TotalPlayerCountText)); + + PodiumFirst = mappedPlayers.ElementAtOrDefault(0); + PodiumSecond = mappedPlayers.ElementAtOrDefault(1); + PodiumThird = mappedPlayers.ElementAtOrDefault(2); + + foreach (var player in mappedPlayers.Skip(3)) + { + if (cancellationToken.IsCancellationRequested) + return; + + RemainingPlayers.Add(player); + + try + { + await Task.Delay(12, cancellationToken); + } + catch (OperationCanceledException) + { + return; + } + } + + SetDataState(); + } + + private LeaderboardPlayerItem CreateLeaderboardPlayer(RwfcLeaderboardEntry entry, int rank, int index) + { + var friendCode = entry.FriendCode ?? string.Empty; + var badges = string.IsNullOrWhiteSpace(friendCode) ? [] : BadgeService.GetBadges(friendCode); + var primaryBadge = badges.FirstOrDefault(BadgeVariant.None); + + return new() + { + Rank = rank, + PlacementLabel = GetPlacementLabel(rank), + Name = string.IsNullOrWhiteSpace(entry.Name) ? "Unknown Player" : entry.Name, + FriendCode = friendCode, + VrText = entry.Vr?.ToString("N0") ?? "--", + Mii = DeserializeMii(entry.MiiData), + PrimaryBadge = primaryBadge, + HasBadge = primaryBadge != BadgeVariant.None, + IsSuspicious = entry.IsSuspicious, + IsEvenRow = index % 2 == 0, + }; + } + + private static int ResolveRank(RwfcLeaderboardEntry entry, int index) + { + if (entry.Rank is > 0 and <= 50000) + return entry.Rank.Value; + + if (entry.ActiveRank is > 0 and <= 50000) + return entry.ActiveRank.Value; + + return index + 1; + } + + private static string GetPlacementLabel(int rank) => + rank switch + { + 1 => "Champion", + 2 => "2nd Place", + 3 => "3rd Place", + _ => $"#{rank}", + }; + + private static Mii? DeserializeMii(string? miiData) + { + if (string.IsNullOrWhiteSpace(miiData)) + return null; + + try + { + var result = MiiSerializer.Deserialize(miiData); + return result.IsSuccess ? result.Value : null; + } + catch + { + return null; + } + } + + private void SetLoadingState() + { + HasError = false; + HasNoData = false; + HasData = false; + ErrorMessage = string.Empty; + } + + private void SetErrorState(string message) + { + HasError = true; + HasNoData = false; + HasData = false; + ErrorMessage = string.IsNullOrWhiteSpace(message) ? "Failed to load leaderboard." : message; + } + + private void SetEmptyState() + { + HasError = false; + HasNoData = true; + HasData = false; + } + + private void SetDataState() + { + HasError = false; + HasNoData = false; + HasData = true; + } + + private void ClearLeaderboardData() + { + PodiumFirst = null; + PodiumSecond = null; + PodiumThird = null; + RemainingPlayers.Clear(); + _loadedPlayerCount = 0; + + OnPropertyChanged(nameof(TotalPlayerCountText)); + OnPropertyChanged(nameof(RemainingCountText)); + } + + private void CancelCurrentLoad() + { + if (_loadCts == null) + return; + + _loadCts.Cancel(); + _loadCts.Dispose(); + _loadCts = null; + } + + private void CopyFriendCode_OnClick(object sender, RoutedEventArgs e) + { + var player = GetContextPlayer(sender); + if (player == null || string.IsNullOrWhiteSpace(player.FriendCode)) + return; + + TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(player.FriendCode); + ViewUtils.ShowSnackbar(Phrases.SnackbarSuccess_CopiedFC); + } + + private void OpenCarousel_OnClick(object sender, RoutedEventArgs e) + { + var player = GetContextPlayer(sender); + if (player?.FirstMii == null) + return; + + new MiiCarouselWindow().SetMii(player.FirstMii).Show(); + } + + private void ViewProfile_OnClick(object sender, RoutedEventArgs e) + { + var player = GetContextPlayer(sender); + if (player == null || string.IsNullOrWhiteSpace(player.FriendCode)) + return; + + new PlayerProfileWindow(player.FriendCode).Show(); + } + + private async void AddFriend_OnClick(object sender, RoutedEventArgs e) + { + var player = GetContextPlayer(sender); + if (player == null) + return; + + if (player.FirstMii == null) + { + ViewUtils.ShowSnackbar("This player has no valid Mii data.", ViewUtils.SnackbarType.Warning); + return; + } + + var focusedUserIndex = (int)SettingsManager.FOCUSSED_USER.Get(); + if (focusedUserIndex is < 0 or > 3) + { + ViewUtils.ShowSnackbar("Invalid license selected.", ViewUtils.SnackbarType.Warning); + return; + } + + var activeUserPid = FriendCodeGenerator.FriendCodeToProfileId(GameDataService.ActiveUser.FriendCode); + if (activeUserPid == 0) + { + ViewUtils.ShowSnackbar("Select a valid license before adding friends.", ViewUtils.SnackbarType.Warning); + return; + } + + if (GameDataService.ActiveCurrentFriends.Count >= 30) + { + ViewUtils.ShowSnackbar("Your friend list is full.", ViewUtils.SnackbarType.Warning); + return; + } + + var normalizedFriendCodeResult = NormalizeFriendCode(player.FriendCode); + if (normalizedFriendCodeResult.IsFailure) + { + ViewUtils.ShowSnackbar(normalizedFriendCodeResult.Error.Message, ViewUtils.SnackbarType.Warning); + return; + } + + var normalizedFriendCode = normalizedFriendCodeResult.Value; + var friendProfileId = FriendCodeGenerator.FriendCodeToProfileId(normalizedFriendCode); + if (activeUserPid == friendProfileId) + { + ViewUtils.ShowSnackbar("You cannot add your own friend code.", ViewUtils.SnackbarType.Warning); + return; + } + + var duplicateFriend = GameDataService.ActiveCurrentFriends.Any(friend => + { + var existingPid = FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode); + return existingPid != 0 && existingPid == friendProfileId; + }); + + if (duplicateFriend) + { + ViewUtils.ShowSnackbar("This friend is already in your list.", ViewUtils.SnackbarType.Warning); + return; + } + + var profile = new PlayerProfileResponse + { + Name = player.Name, + FriendCode = normalizedFriendCode, + Vr = int.TryParse(player.VrText.Replace(",", string.Empty), out var vr) ? vr : 0, + }; + + var shouldAdd = await new AddFriendConfirmationWindow(profile, player.FirstMii).AwaitAnswer(); + if (!shouldAdd) + return; + + var addResult = GameDataService.AddFriend(focusedUserIndex, normalizedFriendCode, player.FirstMii, (uint)Math.Max(profile.Vr, 0)); + + if (addResult.IsFailure) + { + ViewUtils.ShowSnackbar(addResult.Error.Message, ViewUtils.SnackbarType.Warning); + return; + } + + ViewUtils.GetLayout().UpdateFriendCount(); + ViewUtils.ShowSnackbar($"Added {player.Name} to your friend list."); + } + + private static LeaderboardPlayerItem? GetContextPlayer(object sender) + { + if (sender is not Control control) + return null; + return control.DataContext as LeaderboardPlayerItem; + } + + private static OperationResult NormalizeFriendCode(string friendCode) + { + if (string.IsNullOrWhiteSpace(friendCode)) + return Fail("Friend code cannot be empty."); + + var digits = new string(friendCode.Where(char.IsDigit).ToArray()); + if (digits.Length != 12 || !ulong.TryParse(digits, out _)) + return Fail("Friend code must be exactly 12 digits."); + + var formatted = $"{digits[..4]}-{digits.Substring(4, 4)}-{digits.Substring(8, 4)}"; + var profileId = FriendCodeGenerator.FriendCodeToProfileId(formatted); + if (profileId == 0) + return Fail("Invalid friend code."); + + return formatted; + } + + public new event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new(propertyName)); + } +} diff --git a/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs b/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs new file mode 100644 index 00000000..ddd4dbd3 --- /dev/null +++ b/WheelWizard/Views/Pages/LeaderboardPlayerItem.cs @@ -0,0 +1,26 @@ +using WheelWizard.WheelWizardData.Domain; +using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; + +namespace WheelWizard.Views.Pages; + +public sealed class LeaderboardPlayerItem +{ + public required int Rank { get; init; } + public required string PlacementLabel { get; init; } + public required string Name { get; init; } + public required string FriendCode { get; init; } + public required string VrText { get; init; } + public Mii? Mii { get; init; } + public BadgeVariant PrimaryBadge { get; init; } + public bool HasBadge { get; init; } + public bool IsSuspicious { get; init; } + public bool IsEvenRow { get; init; } + + // Keep parity with RoomDetailsPage player template bindings. + public string VrDisplay => VrText; + public Mii? FirstMii => Mii; + public bool HasBadges => HasBadge; + public bool IsTopLeaderboardPlayer => true; + public string TopLabel => $"#{Rank}"; + public bool IsOpenHost => false; +} From c3f0a41c1caa9abb410626b7a86d5dc64597ec7c Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:36:50 +0100 Subject: [PATCH 04/50] Add leaderboard caching and loading UI --- .../RrRooms/RrLeaderboardSingletonService.cs | 83 ++++++++++- .../WhWzLibrary/LeaderboardPodiumCard.axaml | 2 +- .../WhWzLibrary/PlayerListItem.axaml | 20 ++- WheelWizard/Views/Pages/LeaderboardPage.axaml | 27 +++- .../Views/Pages/LeaderboardPage.axaml.cs | 134 +++++++++++------- 5 files changed, 205 insertions(+), 61 deletions(-) diff --git a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs index bedddc6c..54dac2c9 100644 --- a/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs +++ b/WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using WheelWizard.Shared.Services; namespace WheelWizard.RrRooms; @@ -7,11 +8,89 @@ public interface IRrLeaderboardSingletonService Task>> GetTopPlayersAsync(int limit = 50); } -public class RrLeaderboardSingletonService(IApiCaller apiCaller) : IRrLeaderboardSingletonService +public class RrLeaderboardSingletonService(IApiCaller apiCaller, ILogger logger) + : IRrLeaderboardSingletonService { + private static readonly TimeSpan CacheLifetime = TimeSpan.FromSeconds(90); + private readonly SemaphoreSlim _refreshGate = new(1, 1); + private readonly object _cacheLock = new(); + + private DateTimeOffset _cacheFetchedAt = DateTimeOffset.MinValue; + private List _cachedEntries = []; + public async Task>> GetTopPlayersAsync(int limit = 50) { var boundedLimit = Math.Clamp(limit, 1, 200); - return await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit)); + + if (TryGetFreshCache(boundedLimit, out var freshCache)) + return freshCache; + + await _refreshGate.WaitAsync(); + + try + { + if (TryGetFreshCache(boundedLimit, out freshCache)) + return freshCache; + + var fetchResult = await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit)); + if (fetchResult.IsFailure) + { + if (TryGetAnyCache(boundedLimit, out var staleCache)) + { + logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit); + return staleCache; + } + + return fetchResult; + } + + lock (_cacheLock) + { + _cachedEntries = fetchResult.Value.ToList(); + _cacheFetchedAt = DateTimeOffset.UtcNow; + } + + return TrimToLimit(fetchResult.Value, boundedLimit); + } + finally + { + _refreshGate.Release(); + } + } + + private bool TryGetFreshCache(int limit, out OperationResult> result) + { + lock (_cacheLock) + { + var hasFreshCache = DateTimeOffset.UtcNow - _cacheFetchedAt <= CacheLifetime; + if (!hasFreshCache || _cachedEntries.Count < limit) + { + result = default!; + return false; + } + + result = TrimToLimit(_cachedEntries, limit); + return true; + } + } + + private bool TryGetAnyCache(int limit, out OperationResult> result) + { + lock (_cacheLock) + { + if (_cachedEntries.Count < limit) + { + result = default!; + return false; + } + + result = TrimToLimit(_cachedEntries, limit); + return true; + } + } + + private static OperationResult> TrimToLimit(IEnumerable entries, int limit) + { + return entries.Take(limit).ToList(); } } diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml index 64104b03..41938974 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml +++ b/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml @@ -66,7 +66,7 @@ BorderThickness="1" CornerRadius="20" Padding="10,14,10,10" - Margin="4,5,4,5" + Margin="4,8,4,5" ClipToBounds="True" BoxShadow="0 18 30 0 #22000000"> - - + - \ No newline at end of file + + + + + diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml b/WheelWizard/Views/Pages/LeaderboardPage.axaml index d11de3cb..202b4b81 100644 --- a/WheelWizard/Views/Pages/LeaderboardPage.axaml +++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml @@ -55,6 +55,14 @@ + + @@ -90,6 +98,21 @@ + + + + + + IsTopPlayer="True" + TopLabel="{Binding TopLabel}"> _isLoading; + private set + { + if (_isLoading == value) + return; + _isLoading = value; + OnPropertyChanged(nameof(IsLoading)); + } + } + public bool HasNoData { get => _hasNoData; @@ -98,46 +124,25 @@ private set public LeaderboardPlayerItem? PodiumFirst { - get => _podiumFirst; - private set - { - if (_podiumFirst == value) - return; - _podiumFirst = value; - OnPropertyChanged(nameof(PodiumFirst)); - OnPropertyChanged(nameof(HasPodiumFirst)); - } + get => _podiumFirst ?? EmptyPodiumPlayer; + private set => SetPodiumPlayer(ref _podiumFirst, value, nameof(PodiumFirst), nameof(HasPodiumFirst)); } public LeaderboardPlayerItem? PodiumSecond { - get => _podiumSecond; - private set - { - if (_podiumSecond == value) - return; - _podiumSecond = value; - OnPropertyChanged(nameof(PodiumSecond)); - OnPropertyChanged(nameof(HasPodiumSecond)); - } + get => _podiumSecond ?? EmptyPodiumPlayer; + private set => SetPodiumPlayer(ref _podiumSecond, value, nameof(PodiumSecond), nameof(HasPodiumSecond)); } public LeaderboardPlayerItem? PodiumThird { - get => _podiumThird; - private set - { - if (_podiumThird == value) - return; - _podiumThird = value; - OnPropertyChanged(nameof(PodiumThird)); - OnPropertyChanged(nameof(HasPodiumThird)); - } + get => _podiumThird ?? EmptyPodiumPlayer; + private set => SetPodiumPlayer(ref _podiumThird, value, nameof(PodiumThird), nameof(HasPodiumThird)); } - public bool HasPodiumFirst => PodiumFirst != null; - public bool HasPodiumSecond => PodiumSecond != null; - public bool HasPodiumThird => PodiumThird != null; + public bool HasPodiumFirst => _podiumFirst != null; + public bool HasPodiumSecond => _podiumSecond != null; + public bool HasPodiumThird => _podiumThird != null; public LeaderboardPage() { @@ -181,6 +186,7 @@ private async Task ReloadLeaderboardAsync() SetLoadingState(); ClearLeaderboardData(); + await Task.Yield(); var leaderboardResult = await LeaderboardService.GetTopPlayersAsync(50); if (cancellationToken.IsCancellationRequested) @@ -204,7 +210,21 @@ private async Task ReloadLeaderboardAsync() return; } - var mappedPlayers = orderedEntries.Select((entry, index) => CreateLeaderboardPlayer(entry.Entry, entry.Rank, index)).ToList(); + List mappedPlayers; + try + { + mappedPlayers = await Task.Run( + () => orderedEntries.Select((entry, index) => CreateLeaderboardPlayer(entry.Entry, entry.Rank, index)).ToList(), + cancellationToken + ); + } + catch (OperationCanceledException) + { + return; + } + + if (cancellationToken.IsCancellationRequested) + return; _loadedPlayerCount = mappedPlayers.Count; OnPropertyChanged(nameof(TotalPlayerCountText)); @@ -213,21 +233,12 @@ private async Task ReloadLeaderboardAsync() PodiumSecond = mappedPlayers.ElementAtOrDefault(1); PodiumThird = mappedPlayers.ElementAtOrDefault(2); + if (cancellationToken.IsCancellationRequested) + return; + foreach (var player in mappedPlayers.Skip(3)) { - if (cancellationToken.IsCancellationRequested) - return; - RemainingPlayers.Add(player); - - try - { - await Task.Delay(12, cancellationToken); - } - catch (OperationCanceledException) - { - return; - } } SetDataState(); @@ -279,19 +290,37 @@ private static string GetPlacementLabel(int rank) => if (string.IsNullOrWhiteSpace(miiData)) return null; - try - { - var result = MiiSerializer.Deserialize(miiData); - return result.IsSuccess ? result.Value : null; - } - catch - { + // Mii block payload should be ~100 base64 chars (74 bytes decoded). + // Guarding the size avoids expensive decode failures for large non-Mii payloads. + if (miiData.Length is < 90 or > 120) return null; - } + + var buffer = new byte[MiiSerializer.MiiBlockSize]; + if (!Convert.TryFromBase64String(miiData, buffer, out var bytesWritten) || bytesWritten != MiiSerializer.MiiBlockSize) + return null; + + var result = MiiSerializer.Deserialize(buffer); + return result.IsSuccess ? result.Value : null; + } + + private void SetPodiumPlayer( + ref LeaderboardPlayerItem? field, + LeaderboardPlayerItem? value, + string propertyName, + string hasPropertyName + ) + { + if (field == value) + return; + + field = value; + OnPropertyChanged(propertyName); + OnPropertyChanged(hasPropertyName); } private void SetLoadingState() { + IsLoading = true; HasError = false; HasNoData = false; HasData = false; @@ -300,6 +329,7 @@ private void SetLoadingState() private void SetErrorState(string message) { + IsLoading = false; HasError = true; HasNoData = false; HasData = false; @@ -308,6 +338,7 @@ private void SetErrorState(string message) private void SetEmptyState() { + IsLoading = false; HasError = false; HasNoData = true; HasData = false; @@ -315,6 +346,7 @@ private void SetEmptyState() private void SetDataState() { + IsLoading = false; HasError = false; HasNoData = false; HasData = true; From 36520ff5e99000a6c975e4700af68d1e52fa845c Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:39:41 +0100 Subject: [PATCH 05/50] Update LeaderboardPage.axaml --- WheelWizard/Views/Pages/LeaderboardPage.axaml | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml b/WheelWizard/Views/Pages/LeaderboardPage.axaml index 202b4b81..f60403ab 100644 --- a/WheelWizard/Views/Pages/LeaderboardPage.axaml +++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml @@ -201,7 +201,21 @@ Mii="{Binding PodiumSecond.Mii}" ShowBadge="{Binding PodiumSecond.HasBadge}" BadgeVariant="{Binding PodiumSecond.PrimaryBadge}" - IsSuspicious="{Binding PodiumSecond.IsSuspicious}" /> + IsSuspicious="{Binding PodiumSecond.IsSuspicious}"> + + + + + + + + + + + IsSuspicious="{Binding PodiumFirst.IsSuspicious}"> + + + + + + + + + + + IsSuspicious="{Binding PodiumThird.IsSuspicious}"> + + + + + + + + + + From 0dcc00961039c22331814dffaaa19089faa60c3c Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:08:01 +0100 Subject: [PATCH 06/50] Add friend/online flags and join-room UI --- WheelWizard/Models/RRInfo/RrPlayer.cs | 1 + WheelWizard/Services/LiveData/RRLiveRooms.cs | 24 +++++- .../WhWzLibrary/PlayerListItem.axaml | 28 ++++++- .../WhWzLibrary/PlayerListItem.axaml.cs | 73 +++++++++++++++---- WheelWizard/Views/Pages/LeaderboardPage.axaml | 3 + .../Views/Pages/LeaderboardPage.axaml.cs | 46 +++++++++++- .../Views/Pages/LeaderboardPlayerItem.cs | 2 + WheelWizard/Views/Pages/RoomsPage.axaml | 2 +- 8 files changed, 156 insertions(+), 23 deletions(-) diff --git a/WheelWizard/Models/RRInfo/RrPlayer.cs b/WheelWizard/Models/RRInfo/RrPlayer.cs index cadd5c18..7a12bca0 100644 --- a/WheelWizard/Models/RRInfo/RrPlayer.cs +++ b/WheelWizard/Models/RRInfo/RrPlayer.cs @@ -14,6 +14,7 @@ public class RrPlayer : IEquatable public bool IsOpenHost { get; set; } public bool IsSuspended { get; set; } + public bool IsFriend { get; set; } public int? LeaderboardRank { get; set; } public bool IsTopLeaderboardPlayer => LeaderboardRank.HasValue; diff --git a/WheelWizard/Services/LiveData/RRLiveRooms.cs b/WheelWizard/Services/LiveData/RRLiveRooms.cs index 99aef535..e4594769 100644 --- a/WheelWizard/Services/LiveData/RRLiveRooms.cs +++ b/WheelWizard/Services/LiveData/RRLiveRooms.cs @@ -1,9 +1,11 @@ using WheelWizard.Models.RRInfo; using WheelWizard.RrRooms; +using WheelWizard.Utilities.Generators; using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.Views; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement; +using WheelWizard.WiiManagement.GameLicense; using WheelWizard.WiiManagement.MiiManagement; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; @@ -26,6 +28,7 @@ protected override async Task ExecuteTaskAsync() var whWzService = App.Services.GetRequiredService(); var roomsService = App.Services.GetRequiredService(); var leaderboardService = App.Services.GetRequiredService(); + var gameLicenseService = App.Services.GetRequiredService(); var roomsTask = roomsService.GetRoomsAsync(); var leaderboardTask = leaderboardService.GetTopPlayersAsync(50); @@ -59,7 +62,14 @@ 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 friendProfileIds = gameLicenseService + .ActiveCurrentFriends.Select(friend => FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode)) + .Where(profileId => profileId != 0) + .ToHashSet(); + + var rrRooms = splitRaw + .Select(room => MapRoom(room, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) + .ToList(); CurrentRooms = rrRooms; } @@ -68,7 +78,8 @@ private static RrRoom MapRoom( RwfcRoomStatusRoom room, IWhWzDataSingletonService whWzService, IReadOnlyDictionary leaderboardByPid, - IReadOnlyDictionary leaderboardByFriendCode + IReadOnlyDictionary leaderboardByFriendCode, + IReadOnlySet friendProfileIds ) { return new() @@ -78,7 +89,9 @@ IReadOnlyDictionary leaderboardByFriendCode Type = room.Type, Suspend = room.Suspend, Rk = room.Rk, - Players = room.Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList(), + Players = room + .Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) + .ToList(), }; } @@ -86,7 +99,8 @@ private static RrPlayer MapPlayer( RwfcRoomStatusPlayer p, IWhWzDataSingletonService whWzService, IReadOnlyDictionary leaderboardByPid, - IReadOnlyDictionary leaderboardByFriendCode + IReadOnlyDictionary leaderboardByFriendCode, + IReadOnlySet friendProfileIds ) { Mii? mii = null; @@ -106,6 +120,7 @@ IReadOnlyDictionary leaderboardByFriendCode } var friendCode = p.FriendCode ?? string.Empty; + var profileId = FriendCodeGenerator.FriendCodeToProfileId(friendCode); var leaderboardEntry = GetLeaderboardEntry(p, friendCode, leaderboardByPid, leaderboardByFriendCode); @@ -122,6 +137,7 @@ IReadOnlyDictionary leaderboardByFriendCode Mii = mii, BadgeVariants = whWzService.GetBadges(friendCode), LeaderboardRank = leaderboardEntry?.Rank ?? leaderboardEntry?.ActiveRank, + IsFriend = profileId != 0 && friendProfileIds.Contains(profileId), }; } diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml index 9dbe492d..c463717a 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml +++ b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml @@ -73,7 +73,7 @@ Margin="10,0,0,0" /> - + + + - + + diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs index 969ac2b6..8b26cc67 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs +++ b/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs @@ -1,6 +1,7 @@ -using Avalonia; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; @@ -8,6 +9,13 @@ namespace WheelWizard.Views.Components; public class PlayerListItem : TemplatedControl { + private Button? _joinRoomButton; + + public static readonly StyledProperty IsOnlineProperty = AvaloniaProperty.Register(nameof(IsOnline)); + public static readonly StyledProperty ShowJoinRoomButtonProperty = AvaloniaProperty.Register( + nameof(ShowJoinRoomButton) + ); + public static readonly StyledProperty HasBadgesProperty = AvaloniaProperty.Register(nameof(HasBadges)); public static readonly StyledProperty IsOpenHostProperty = AvaloniaProperty.Register(nameof(IsOpenHost)); @@ -25,6 +33,18 @@ public bool HasBadges set => SetValue(HasBadgesProperty, value); } + public bool IsOnline + { + get => GetValue(IsOnlineProperty); + set => SetValue(IsOnlineProperty, value); + } + + public bool ShowJoinRoomButton + { + get => GetValue(ShowJoinRoomButtonProperty); + set => SetValue(ShowJoinRoomButtonProperty, value); + } + public bool IsOpenHost { get => GetValue(IsOpenHostProperty); @@ -85,23 +105,50 @@ public string UserName set => SetValue(UserNameProperty, value); } + public static readonly StyledProperty?> JoinRoomActionProperty = AvaloniaProperty.Register< + PlayerListItem, + Action? + >(nameof(JoinRoomAction)); + + public Action? JoinRoomAction + { + get => GetValue(JoinRoomActionProperty); + set => SetValue(JoinRoomActionProperty, value); + } + + private void JoinRoom_OnClick(object? sender, RoutedEventArgs e) + { + if (string.IsNullOrWhiteSpace(FriendCode)) + return; + + JoinRoomAction?.Invoke(FriendCode); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - var container = e.NameScope.Find("PART_BadgeContainer"); - if (container == null) - return; - container.Children.Clear(); - var badges = App - .Services.GetRequiredService() - .GetBadges(FriendCode) - .Select(variant => new Badge { Variant = variant }); - foreach (var badge in badges) + var container = e.NameScope.Find("PART_BadgeContainer"); + if (container != null) { - badge.Height = 30; - badge.Width = 30; - container.Children.Add(badge); + container.Children.Clear(); + var badges = App + .Services.GetRequiredService() + .GetBadges(FriendCode) + .Select(variant => new Badge { Variant = variant }); + foreach (var badge in badges) + { + badge.Height = 30; + badge.Width = 30; + container.Children.Add(badge); + } } + + if (_joinRoomButton != null) + _joinRoomButton.Click -= JoinRoom_OnClick; + + _joinRoomButton = e.NameScope.Find