From d4d60460eb44a6eea13d27dfc5f80b82ef661a54 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:44:06 +0530 Subject: [PATCH 01/26] Refactor Core and enhance game management features Refactored the `Core` class to introduce `SavedGame` and `SavedGameCollection` for managing saved games. Added `LoadGames`, `SaveGames`, and `RemoveGame` methods, improving game lifecycle management. Updated `InstallGame` to handle `Game` objects and save state changes. Enhanced the `Game` class with a `FromTuple` method, improved error handling, and added offline mode checks. Improved `BaseSettingsService` to support file-based storage with `SaveToFileAsync` and `LoadFromFileAsync`. Updated `Set` and `Get` methods to handle file-based settings. Refined mod loader installers (`Fabric`, `Forge`, `Quilt`, `ModLoaderRouter`) with better error handling and offline support. Upgraded the solution to Visual Studio 18 and removed unused build configurations. Centralized settings keys in the new `SettingsKeys` class. Enhanced logging and exception handling across the codebase for better debugging and user notifications. --- Emerald.CoreX/Core.cs | 145 +++++++++++++++++-- Emerald.CoreX/Game.cs | 30 +++- Emerald.CoreX/Helpers/BaseSettingsService.cs | 81 +++++++++-- Emerald.CoreX/Helpers/SettingsKeys.cs | 12 ++ Emerald.CoreX/Installers/Fabric.cs | 2 +- Emerald.CoreX/Installers/Forge.cs | 4 +- Emerald.CoreX/Installers/ModLoaderRouter.cs | 2 +- Emerald.CoreX/Installers/Quilt.cs | 2 +- Emerald.CoreX/Versions/Version.cs | 2 +- Emerald.sln | 56 +------ 10 files changed, 241 insertions(+), 95 deletions(-) create mode 100644 Emerald.CoreX/Helpers/SettingsKeys.cs diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 2f461eb0..57956d5d 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -1,14 +1,23 @@ -using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using CmlLib.Core; +using CmlLib.Core.Utils; using CmlLib.Core.VersionMetadata; -using Emerald.CoreX.Notifications; -using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; using Emerald.CoreX.Helpers; +using Emerald.CoreX.Notifications; using Emerald.Services; +using Microsoft.Extensions.Logging; namespace Emerald.CoreX; -public class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) +public record SavedGame(string Path, Versions.Version Version, Models.GameSettings GameOptions); + +public record SavedGameCollection(string BasePath, SavedGame[] Games); + +public partial class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) : ObservableObject { + public const string GamesFolderName = "EmeraldGames"; public MinecraftLauncher Launcher { get; set; } public bool IsRunning { get; set; } = false; @@ -19,14 +28,80 @@ public class Core(ILogger _logger, INotificationService _notify, BaseSetti public readonly ObservableCollection Games = new(); - private bool initialized = false; + [ObservableProperty] + private bool _initialized = false; public Models.GameSettings GameOptions = new(); - public async Task Refresh() + + private SavedGameCollection[] SavedgamesWithPaths = []; + + public void LoadGames() + { + if (BasePath == null) + { + _logger.LogWarning("Cannot load games, BasePath is not set"); + throw new InvalidOperationException("Cannot load games, BasePath is not set"); + } + + var gamesFolder = Path.Combine(BasePath.BasePath, GamesFolderName); + if (!Path.Exists(gamesFolder)) + { + _logger.LogInformation("Games folder does not exist, creating..."); + Directory.CreateDirectory(gamesFolder); + } + + SavedgamesWithPaths = settingsService.Get(SettingsKeys.SavedGames, [], true); + + var collection = SavedgamesWithPaths.FirstOrDefault(x => x.BasePath == BasePath.BasePath); + if (collection == null) + { + _logger.LogInformation("Saved games paths does not contain any games"); + return; + } + + foreach (var sg in collection.Games) + { + try + { + var game = Game.FromTuple((sg.Path, sg.Version, sg.GameOptions)); + Games.Add(game); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to load game from {dir}: {ex}", sg.Path, ex.Message); + _notify.Error("FailedToLoadGame", $"Failed to load game from {sg.Path}", ex: ex); + } + } + + _logger.LogInformation("Loaded {count} games from", Games.Count); + } + public void SaveGames() { + _logger.LogInformation("Saving {count} games", Games.Count); + + var toSave = Games.Select(x => + new SavedGame(x.Path.BasePath, x.Version, x.Options) + ).ToArray(); + + try + { + var list = SavedgamesWithPaths.ToList(); + list.RemoveAll(x => x.BasePath == BasePath.BasePath); + list.Add(new SavedGameCollection(BasePath.BasePath, toSave)); + + SavedgamesWithPaths = list.ToArray(); + settingsService.Set(SettingsKeys.SavedGames, SavedgamesWithPaths, true); + _logger.LogInformation("Saved {count} games", toSave.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save games"); + throw; + } } + /// /// Initializes the Core with the given Minecraft path and retrieves the list of available vanilla Minecraft versions. /// @@ -44,7 +119,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) GameOptions = settingsService.Get("BaseGameOptions", Models.GameSettings.FromMLaunchOption(new())); _logger.LogInformation("Trying to load vanilla minecraft versions from servers"); - if (!initialized && basePath == null) + if (!Initialized && basePath == null) { _logger.LogInformation("Minecraft Path must be set on first initialize"); throw new InvalidOperationException("Minecraft Path must be set on first initialize"); @@ -54,8 +129,9 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) Launcher = new MinecraftLauncher(basePath); BasePath = basePath; } - - initialized = true; + + LoadGames(); + Initialized = true; var l = await Launcher.GetAllVersionsAsync(not.CancellationToken.Value); @@ -72,7 +148,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) { _logger.LogCritical(ex, "Failed to load vanilla minecraft versions: {ex}", ex.Message); _notify.Complete(not.Id, false, ex.Message, ex); - initialized = false; + Initialized = false; } _logger.LogInformation("Loaded {count} vanilla versions", VanillaVersions.Count); } @@ -83,14 +159,14 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) /// The version of the game to be installed. Must exist in the collection of games. /// Specifies whether to display file progress during installation. /// A task that represents the asynchronous operation of installing the game version. -public async Task InstallGame(Versions.Version version, bool showFileprog = false) +public async Task InstallGame(Game game, bool showFileprog = false) { - + var version = game.Version; + try { - _logger.LogInformation("Installing game {version}", version.BasedOn); - var game = Games.Where(x => x.Equals(version)).First(); + _logger.LogInformation("Installing game {version}", version.BasedOn); if(game == null) { @@ -116,11 +192,13 @@ public void AddGame(Versions.Version version) { _logger.LogInformation("Adding game {version}", version.BasedOn); - var path = Path.Combine( BasePath.BasePath, version.DisplayName); + var path = Path.Combine( BasePath.BasePath, GamesFolderName, version.DisplayName); + + var game = new Game(new(path), GameOptions, version); - var game = new Game(new(path), GameOptions); Games.Add(game); + SaveGames(); var not = _notify.Info( "AddedGame", @@ -137,4 +215,39 @@ public void AddGame(Versions.Version version) ); } } + + public void RemoveGame(Game game, bool deleteFolder = false) + { + try + { + _logger.LogInformation("Removing game {version}", game.Version.BasedOn); + if (!Games.Contains(game)) + { + _logger.LogWarning("Game {version} not found in collection", game.Version.BasedOn); + throw new NullReferenceException($"Game {game.Version.BasedOn} not found in collection"); + } + Games.Remove(game); + SaveGames(); + + if (deleteFolder && Path.Exists(game.Path.BasePath)) + { + _logger.LogInformation("Deleting game folder {path}", game.Path.BasePath); + Directory.Delete(game.Path.BasePath, true); + } + + var not = _notify.Info( + "RemovedGame", + $"{game.Version.DisplayName} based on {game.Version.BasedOn} {game.Version.Type}" + ); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to remove game {version}: {ex}", game.Version.BasedOn, ex.Message); + _notify.Error( + "FailedToRemoveGame", + $"Failed to remove game {game.Version.DisplayName} based on {game.Version.BasedOn} {game.Version.Type}", + ex: ex + ); + } + } } diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index 31cec9b9..ac8639e4 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -11,6 +11,9 @@ namespace Emerald.CoreX; public class Game { + public static Game FromTuple((string Path, Versions.Version version, Models.GameSettings Options) t) + => new(new MinecraftPath(t.Path), t.Options, t.version); + private readonly ILogger _logger; private readonly Notifications.INotificationService _notify; @@ -26,13 +29,14 @@ public class Game /// Represents a Game instance, responsible for managing the installation, configuration, /// and launching of Minecraft versions. /// - public Game(MinecraftPath path, Models.GameSettings options) + public Game(MinecraftPath path, Models.GameSettings options, Versions.Version version) { _notify = Ioc.Default.GetService(); _logger = this.Log(); Launcher = new MinecraftLauncher(); Path = path; Options = options; + Version = version; _logger.LogInformation("Game instance created with path: {Path} and options: {Options}", path, options); } @@ -71,24 +75,33 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = try { - string ver = await Ioc.Default.GetService().RouteAndInitializeAsync(Path, Version); + string? ver = await Ioc.Default.GetService().RouteAndInitializeAsync(Path, Version); _logger.LogInformation("Version initialization completed. Version: {Version}", ver); if (ver == null) { - var vers = await Launcher.GetAllVersionsAsync(); - ver = vers.First(x => x.Name.ToLower().Contains(Version.BasedOn.ToLower())).Name; - _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found. Using {FallbackVersion} instead.", Version.Type, Version.ModVersion, Version.BasedOn, ver); + _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found.", Version.Type, Version.ModVersion, Version.BasedOn); - _notify.Update( + _notify.Complete( not.Id, - message: $"Version {Version.Type} {Version.ModVersion} {Version.BasedOn} not found. Using {ver} instead.", - isIndeterminate: false + message: $"Version {Version.Type} {Version.ModVersion} {Version.BasedOn} not found. Check your internet connection.", + success: false ); return; } + if (isOffline) //checking if verison actually exists + { + + var vers = await Launcher.GetAllVersionsAsync(); + var mver = vers.Where(x => x.Name == ver).First(); + if (mver == null) + { + _logger.LogWarning("Version {Version} not found in offline mode. Can't proceed installation.", ver); + throw new NullReferenceException($"Version {ver} not found in offline mode. Can't proceed installation."); + } + } (string Files, string bytes, double prog) prog = (string.Empty, string.Empty, 0); @@ -125,6 +138,7 @@ await Launcher.InstallAsync( catch (Exception ex) { _logger.LogError(ex, "An error occurred during version installation."); + _notify.Complete(not.Id, false, "Installation Failed", ex); } } diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index f0649da2..71ba2279 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using System.Text.Json; using System; -using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Threading.Tasks; using Windows.Storage; @@ -11,6 +10,10 @@ namespace Emerald.Services; public class BaseSettingsService { private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions = new() + { + WriteIndented = true // Makes file easier to read/debug + }; public event EventHandler? APINoMatch; @@ -18,27 +21,43 @@ public BaseSettingsService(ILogger logger) { _logger = logger; } - public void Set(string key, T value) + + public void Set(string key, T value, bool storeInFile = false) { try { - string json = JsonSerializer.Serialize(value); - ApplicationData.Current.LocalSettings.Values[key] = json; + if (storeInFile) + { + SaveToFileAsync(key, value).Wait(); + } + else + { + string json = JsonSerializer.Serialize(value, _jsonOptions); + ApplicationData.Current.LocalSettings.Values[key] = json; + } } catch (Exception ex) { - _logger.LogError(ex, "Error deserializing key '{Key}' from settings", key); + _logger.LogError(ex, "Error saving key '{Key}' to settings", key); } } - public T Get(string key, T defaultVal) + public T Get(string key, T defaultVal, bool loadFromFile = false) { try { - string json = ApplicationData.Current.LocalSettings.Values[key] as string; - if (!string.IsNullOrWhiteSpace(json)) + if (loadFromFile) + { + return LoadFromFileAsync(key, defaultVal).GetAwaiter().GetResult(); + } + else { - return JsonSerializer.Deserialize(json); + if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out object? value) + && value is string json + && !string.IsNullOrWhiteSpace(json)) + { + return JsonSerializer.Deserialize(json) ?? defaultVal; + } } } catch (Exception ex) @@ -47,8 +66,48 @@ public T Get(string key, T defaultVal) } // Save default value if deserialization fails or key is missing - Set(key, defaultVal); + Set(key, defaultVal, storeInFile: loadFromFile); return defaultVal; } + + private async Task SaveToFileAsync(string key, T value) + { + try + { + string fileName = $"{key}.json"; + StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync( + fileName, CreationCollisionOption.ReplaceExisting); + + string json = JsonSerializer.Serialize(value, _jsonOptions); + await FileIO.WriteTextAsync(file, json); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error writing key '{Key}' to file storage", key); + throw; + } + } + + private async Task LoadFromFileAsync(string key, T defaultVal) + { + try + { + string fileName = $"{key}.json"; + StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName); + + string json = await FileIO.ReadTextAsync(file); + return JsonSerializer.Deserialize(json) ?? defaultVal; + } + catch (FileNotFoundException) + { + // Return default if file doesn't exist + return defaultVal; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading key '{Key}' from file storage", key); + return defaultVal; + } + } } diff --git a/Emerald.CoreX/Helpers/SettingsKeys.cs b/Emerald.CoreX/Helpers/SettingsKeys.cs new file mode 100644 index 00000000..2b47bfb1 --- /dev/null +++ b/Emerald.CoreX/Helpers/SettingsKeys.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Emerald.CoreX.Helpers; + +public static class SettingsKeys +{ + public const string SavedGames = "SavedGames"; +} diff --git a/Emerald.CoreX/Installers/Fabric.cs b/Emerald.CoreX/Installers/Fabric.cs index 1ecf5987..c368dd5a 100644 --- a/Emerald.CoreX/Installers/Fabric.cs +++ b/Emerald.CoreX/Installers/Fabric.cs @@ -69,7 +69,7 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str { this.Log().LogWarning("Fabric Loader installation is not supported offline. sending the version name"); _notify.Complete(not.Id, false, "Fabric Loader installation is not supported offline. Passed the version name."); - return FabricInstaller.GetVersionName(mcversion, modversion ?? (await fabricInstaller.GetFirstLoader(modversion)).Version); + return FabricInstaller.GetVersionName(mcversion, modversion ?? (await fabricInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } diff --git a/Emerald.CoreX/Installers/Forge.cs b/Emerald.CoreX/Installers/Forge.cs index c6c48a92..5529d644 100644 --- a/Emerald.CoreX/Installers/Forge.cs +++ b/Emerald.CoreX/Installers/Forge.cs @@ -63,10 +63,10 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str this.Log().LogInformation("Installing Forge Loader for {mcversion}", mcversion); try { - - var forge = new ForgeInstaller(new(path)); + //TODO: check whether ForgeInstaller supports offline installation + string? versionName = null; if (modversion == null) diff --git a/Emerald.CoreX/Installers/ModLoaderRouter.cs b/Emerald.CoreX/Installers/ModLoaderRouter.cs index eeadad64..59a981d1 100644 --- a/Emerald.CoreX/Installers/ModLoaderRouter.cs +++ b/Emerald.CoreX/Installers/ModLoaderRouter.cs @@ -16,7 +16,7 @@ public ModLoaderRouter() Installers = Ioc.Default.GetServices(); } - public async Task RouteAndInitializeAsync(MinecraftPath path, Versions.Version version) + public async Task RouteAndInitializeAsync(MinecraftPath path, Versions.Version version) { if (version.Type == Versions.Type.Vanilla) diff --git a/Emerald.CoreX/Installers/Quilt.cs b/Emerald.CoreX/Installers/Quilt.cs index 33459c96..2a567526 100644 --- a/Emerald.CoreX/Installers/Quilt.cs +++ b/Emerald.CoreX/Installers/Quilt.cs @@ -69,7 +69,7 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str { this.Log().LogWarning("Fabric Loader installation is not supported offline. sending the version name"); _notify.Complete(not.Id, false, "Fabric Loader installation is not supported offline. Passed the version name."); - return QuiltInstaller.GetVersionName(mcversion, modversion ?? (await QuiltInstaller.GetFirstLoader(modversion)).Version); + return QuiltInstaller.GetVersionName(mcversion, modversion ?? (await QuiltInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } diff --git a/Emerald.CoreX/Versions/Version.cs b/Emerald.CoreX/Versions/Version.cs index acde23e9..cefc62ad 100644 --- a/Emerald.CoreX/Versions/Version.cs +++ b/Emerald.CoreX/Versions/Version.cs @@ -28,7 +28,7 @@ public class Version public CmlLib.Core.VersionMetadata.IVersionMetadata? Metadata { get; set; } - public string DisplayName; + public string DisplayName; //This Should be unique among all versions public override bool Equals(object? obj) { diff --git a/Emerald.sln b/Emerald.sln index bb5184e0..cddebe04 100644 --- a/Emerald.sln +++ b/Emerald.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35103.136 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11018.127 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emerald", "Emerald\Emerald.csproj", "{9D3213F4-E514-4E7D-872A-725DB4872436}" EndProject @@ -32,10 +32,6 @@ Global Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 - SkipOld|Any CPU = SkipOld|Any CPU - SkipOld|arm64 = SkipOld|arm64 - SkipOld|x64 = SkipOld|x64 - SkipOld|x86 = SkipOld|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9D3213F4-E514-4E7D-872A-725DB4872436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -62,18 +58,6 @@ Global {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.ActiveCfg = Release|Any CPU {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.Build.0 = Release|Any CPU {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.Deploy.0 = Release|Any CPU {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|Any CPU.ActiveCfg = Debug|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|Any CPU.Build.0 = Debug|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|arm64.ActiveCfg = Debug|arm64 @@ -90,14 +74,6 @@ Global {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x64.Build.0 = Release|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x86.ActiveCfg = Release|x86 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x86.Build.0 = Release|x86 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|Any CPU.ActiveCfg = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|Any CPU.Build.0 = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|arm64.ActiveCfg = Debug|arm64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|arm64.Build.0 = Debug|arm64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x64.ActiveCfg = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x64.Build.0 = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x86.ActiveCfg = Debug|x86 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x86.Build.0 = Debug|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.ActiveCfg = Debug|x64 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.Build.0 = Debug|x64 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.Deploy.0 = Debug|x64 @@ -122,18 +98,6 @@ Global {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.ActiveCfg = Release|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.Build.0 = Release|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.Deploy.0 = Release|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.ActiveCfg = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.Build.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.Deploy.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.ActiveCfg = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.Build.0 = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.Deploy.0 = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.ActiveCfg = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.Build.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.Deploy.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.ActiveCfg = SkipOld|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.Build.0 = SkipOld|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.Deploy.0 = SkipOld|x86 {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -150,14 +114,6 @@ Global {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x64.Build.0 = Release|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x86.ActiveCfg = Release|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x86.Build.0 = Release|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|Any CPU.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|Any CPU.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|arm64.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|arm64.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x64.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x64.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x86.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x86.Build.0 = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -174,14 +130,6 @@ Global {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x64.Build.0 = Release|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x86.ActiveCfg = Release|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x86.Build.0 = Release|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|Any CPU.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|Any CPU.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|arm64.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|arm64.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x64.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x64.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x86.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c8943567ede38d272b48e81fe92c18cc1d85bba2 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:50:30 +0530 Subject: [PATCH 02/26] Refactor to use IBaseSettingsService interface Replaced direct dependency on BaseSettingsService with the IBaseSettingsService interface across the codebase to improve flexibility, modularity, and testability. Updated the `Core` class constructor, `SettingsService` constructor, and dependency injection registration in `App.xaml.cs` to use the interface. Introduced a new `IBaseSettingsService` interface defining the contract for `BaseSettingsService`, including methods for storing and retrieving values and an `APINoMatch` event. Refactored `BaseSettingsService` to implement the new interface. Updated `using` directives to remove unused namespaces and add necessary ones. Updated the project file to target .NET 9.0 for desktop applications. Minor non-functional changes were made to the project file. --- Emerald.CoreX/Core.cs | 2 +- Emerald.CoreX/Emerald.CoreX.csproj | 2 +- Emerald.CoreX/Helpers/BaseSettingsService.cs | 5 ++--- Emerald.CoreX/Services/IBaseSettingsService.cs | 13 +++++++++++++ Emerald/App.xaml.cs | 2 +- Emerald/Services/SettingsService.cs | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 Emerald.CoreX/Services/IBaseSettingsService.cs diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 57956d5d..ea42b9ad 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -15,7 +15,7 @@ public record SavedGame(string Path, Versions.Version Version, Models.GameSettin public record SavedGameCollection(string BasePath, SavedGame[] Games); -public partial class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) : ObservableObject +public partial class Core(ILogger _logger, INotificationService _notify, IBaseSettingsService settingsService) : ObservableObject { public const string GamesFolderName = "EmeraldGames"; public MinecraftLauncher Launcher { get; set; } diff --git a/Emerald.CoreX/Emerald.CoreX.csproj b/Emerald.CoreX/Emerald.CoreX.csproj index 4690b3e5..020b44f6 100644 --- a/Emerald.CoreX/Emerald.CoreX.csproj +++ b/Emerald.CoreX/Emerald.CoreX.csproj @@ -1,4 +1,4 @@ - + diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index 71ba2279..e97faaa5 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -1,13 +1,12 @@ using Microsoft.Extensions.Logging; -using System.Text.Json; using System; -using System.IO; +using System.Text.Json; using System.Threading.Tasks; using Windows.Storage; namespace Emerald.Services; -public class BaseSettingsService +public class BaseSettingsService : IBaseSettingsService { private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions = new() diff --git a/Emerald.CoreX/Services/IBaseSettingsService.cs b/Emerald.CoreX/Services/IBaseSettingsService.cs new file mode 100644 index 00000000..16f6a779 --- /dev/null +++ b/Emerald.CoreX/Services/IBaseSettingsService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace Emerald.Services; + +public interface IBaseSettingsService +{ + event EventHandler? APINoMatch; + + void Set(string key, T value, bool storeInFile = false); + + T Get(string key, T defaultVal, bool loadFromFile = false); +} diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index eb9a60fa..480b7a4f 100644 --- a/Emerald/App.xaml.cs +++ b/Emerald/App.xaml.cs @@ -43,7 +43,7 @@ private void ConfigureServices(IServiceCollection services) //Settings services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); //Notifications services.AddSingleton(); diff --git a/Emerald/Services/SettingsService.cs b/Emerald/Services/SettingsService.cs index 3328d2a9..1f85b9d4 100644 --- a/Emerald/Services/SettingsService.cs +++ b/Emerald/Services/SettingsService.cs @@ -8,7 +8,7 @@ using Windows.Storage; namespace Emerald.Services; -public class SettingsService(BaseSettingsService _baseService, ILogger _logger) +public class SettingsService(IBaseSettingsService _baseService, ILogger _logger) { public Helpers.Settings.JSON.Settings Settings { get; private set; } From bef51cf113527cbf383050425dcc9196faefc759 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Fri, 26 Sep 2025 10:32:03 +0000 Subject: [PATCH 03/26] [CodeFactor] Apply fixes --- Emerald.CoreX/Game.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index ac8639e4..1d8da55f 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -80,7 +80,6 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = if (ver == null) { - _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found.", Version.Type, Version.ModVersion, Version.BasedOn); _notify.Complete( @@ -93,7 +92,6 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = } if (isOffline) //checking if verison actually exists { - var vers = await Launcher.GetAllVersionsAsync(); var mver = vers.Where(x => x.Name == ver).First(); if (mver == null) From c047c5640a4abc9ad5466a07df82da8d7be3e836 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:53:38 +0530 Subject: [PATCH 04/26] Refactor and enhance Minecraft settings UI Refactored `Extensions.Localize` to improve logging and fallback behavior. Enhanced `GameSettings` with new properties and `[NotifyPropertyChangedFor]` attributes. Introduced `MinecraftSettingsUC` user control to encapsulate Minecraft-related settings, replacing inline UI in `GeneralPage`. Simplified `ArgumentsListView` by removing `ArgTemplate` and directly binding to `ObservableCollection`. Improved dependency injection by registering `IBaseSettingsService` in `App.xaml.cs`. Upgraded `Uno.Sdk` to `6.2.39` in `global.json`. Removed legacy code and improved UI structure in `ArgumentsListView` and `MinecraftSettingsUC`. Enhanced logging and localization across the application. --- Emerald.CoreX/Helpers/Extensions.cs | 8 +- Emerald.CoreX/Models/GameSettings.cs | 7 +- Emerald/App.xaml.cs | 3 +- Emerald/Emerald.csproj | 6 + Emerald/Helpers/Extensions.cs | 30 ---- .../MarkupExtensions/LocalizeString.cs | 1 + Emerald/Helpers/MessageBox.cs | 1 + Emerald/Helpers/Settings/JSON.cs | 1 + Emerald/MainPage.xaml.cs | 1 + Emerald/UserControls/ArgumentsListView.xaml | 71 +++----- .../UserControls/ArgumentsListView.xaml.cs | 91 +++------- Emerald/UserControls/MinecraftSettingsUC.xaml | 163 ++++++++++++++++++ .../UserControls/MinecraftSettingsUC.xaml.cs | 115 ++++++++++++ Emerald/Views/Settings/AppearancePage.xaml.cs | 1 + Emerald/Views/Settings/GeneralPage.xaml | 150 +--------------- Emerald/Views/Settings/GeneralPage.xaml.cs | 41 +---- global.json | 4 +- 17 files changed, 362 insertions(+), 332 deletions(-) create mode 100644 Emerald/UserControls/MinecraftSettingsUC.xaml create mode 100644 Emerald/UserControls/MinecraftSettingsUC.xaml.cs diff --git a/Emerald.CoreX/Helpers/Extensions.cs b/Emerald.CoreX/Helpers/Extensions.cs index 58abe91b..f0195728 100644 --- a/Emerald.CoreX/Helpers/Extensions.cs +++ b/Emerald.CoreX/Helpers/Extensions.cs @@ -135,11 +135,15 @@ public static string Localize(this string resourceKey) return cached; } - string s = Windows.ApplicationModel.Resources.ResourceLoader + string? s = Windows.ApplicationModel.Resources.ResourceLoader .GetForViewIndependentUse() .GetString(resourceKey); + if (string.IsNullOrEmpty(s)) - throw new NullReferenceException("ResourceLoader.GetString returned empty/null"); + { + _logger.LogWarning("ResourceLoader.GetString returned empty/null, returning defaultkey"); + return resourceKey; + } cachedResources.AddOrUpdate(resourceKey, s, (_, _) => s); diff --git a/Emerald.CoreX/Models/GameSettings.cs b/Emerald.CoreX/Models/GameSettings.cs index 7da297d7..5c00645d 100644 --- a/Emerald.CoreX/Models/GameSettings.cs +++ b/Emerald.CoreX/Models/GameSettings.cs @@ -9,6 +9,7 @@ using CmlLib.Core.ProcessBuilder; using Emerald.CoreX.Helpers; using CommunityToolkit.Mvvm.ComponentModel; +using System.Runtime.Serialization; namespace Emerald.CoreX.Models; public partial class GameSettings : ObservableObject @@ -16,7 +17,8 @@ public partial class GameSettings : ObservableObject [JsonIgnore] public double MaxRAMinGB => Math.Round((MaximumRamMb / 1024.00), 2); - + + [NotifyPropertyChangedFor(nameof(MaxRAMinGB))] [ObservableProperty] private int _maximumRamMb; @@ -29,12 +31,15 @@ public partial class GameSettings : ObservableObject [ObservableProperty] private bool _isDemo; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private int _screenWidth; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private int _screenHeight; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private bool _fullScreen; diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index 480b7a4f..a93125d7 100644 --- a/Emerald/App.xaml.cs +++ b/Emerald/App.xaml.cs @@ -8,6 +8,7 @@ using System; using Emerald.Helpers; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; namespace Emerald; public partial class App : Application @@ -43,7 +44,7 @@ private void ConfigureServices(IServiceCollection services) //Settings services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); //Notifications services.AddSingleton(); diff --git a/Emerald/Emerald.csproj b/Emerald/Emerald.csproj index 96edc158..2d43a499 100644 --- a/Emerald/Emerald.csproj +++ b/Emerald/Emerald.csproj @@ -120,4 +120,10 @@ + + + MSBuild:Compile + + + diff --git a/Emerald/Helpers/Extensions.cs b/Emerald/Helpers/Extensions.cs index 00fdd6bb..541fd2bc 100644 --- a/Emerald/Helpers/Extensions.cs +++ b/Emerald/Helpers/Extensions.cs @@ -143,36 +143,6 @@ public static string ToMD5(this string s) return sb.ToString(); } - public static string Localize(this string resourceKey) - { - var _logger = Ioc.Default.GetService>(); - try - { - _logger.LogDebug("Localizing {resourceKey}", resourceKey); - - if (cachedResources.TryGetValue(resourceKey, out string cached) && !string.IsNullOrEmpty(cached)) - { - _logger.LogDebug("Found cached {resourceKey} in cache", resourceKey); - return cached; - } - - string s = Windows.ApplicationModel.Resources.ResourceLoader - .GetForViewIndependentUse() - .GetString(resourceKey); - if (string.IsNullOrEmpty(s)) - throw new NullReferenceException("ResourceLoader.GetString returned empty/null"); - - cachedResources.AddOrUpdate(resourceKey, s, (_, _) => s); - - _logger.LogDebug("Localized {resourceKey} to {s}", resourceKey, s); - return string.IsNullOrEmpty(s) ? resourceKey : s; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to localize {resourceKey}", resourceKey); - return resourceKey; - } - } //public static string Localize(this Core.Localized resourceKey) => // resourceKey.ToString().Localize(); diff --git a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs index 495182dc..e45bca8b 100644 --- a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs +++ b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs @@ -1,5 +1,6 @@ using Microsoft.UI.Xaml.Markup; using CommunityToolkit.Mvvm; +using Emerald.CoreX.Helpers; namespace Emerald.Helpers; [MarkupExtensionReturnType(ReturnType = typeof(string))] diff --git a/Emerald/Helpers/MessageBox.cs b/Emerald/Helpers/MessageBox.cs index a40ef08b..e5c54ed1 100644 --- a/Emerald/Helpers/MessageBox.cs +++ b/Emerald/Helpers/MessageBox.cs @@ -1,5 +1,6 @@ using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.Helpers; using Emerald.Helpers.Enums; using Microsoft.UI; diff --git a/Emerald/Helpers/Settings/JSON.cs b/Emerald/Helpers/Settings/JSON.cs index f04fb80b..1aa91bd8 100644 --- a/Emerald/Helpers/Settings/JSON.cs +++ b/Emerald/Helpers/Settings/JSON.cs @@ -10,6 +10,7 @@ using CmlLib.Core.ProcessBuilder; using Emerald.CoreX.Models; using Emerald.CoreX.Store.Modrinth; +using Emerald.CoreX.Helpers; namespace Emerald.Helpers.Settings.JSON; public class JSON : Models.Model diff --git a/Emerald/MainPage.xaml.cs b/Emerald/MainPage.xaml.cs index b31f5ae9..3c015bc6 100644 --- a/Emerald/MainPage.xaml.cs +++ b/Emerald/MainPage.xaml.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography.X509Certificates; using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.CoreX.Installers; using Emerald.Helpers; using Emerald.Models; diff --git a/Emerald/UserControls/ArgumentsListView.xaml b/Emerald/UserControls/ArgumentsListView.xaml index ca819bcd..31249d27 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml +++ b/Emerald/UserControls/ArgumentsListView.xaml @@ -2,61 +2,42 @@ x:Class="Emerald.UserControls.ArgumentsListView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:Emerald.UserControls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" - xmlns:models="using:Emerald.Models" d:DesignWidth="400"> + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + Text="{x:Bind Mode=TwoWay}" /> + + + + diff --git a/Emerald/UserControls/ArgumentsListView.xaml.cs b/Emerald/UserControls/ArgumentsListView.xaml.cs index a8b6622a..13386def 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml.cs +++ b/Emerald/UserControls/ArgumentsListView.xaml.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; +using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; +using Emerald.CoreX.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; @@ -12,83 +14,42 @@ using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; -using System.Collections.ObjectModel; -using Emerald.Models; -using CommonServiceLocator; -using CommunityToolkit.Mvvm.DependencyInjection; - - +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage.Pickers; namespace Emerald.UserControls; - -//Copied from Emerald.UWP public sealed partial class ArgumentsListView : UserControl { - private int count = 0; - public ObservableCollection Args { get; set; } - - public ArgumentsListView() - { - InitializeComponent(); - view.ItemsSource = Source; - UpdateSource(); - } - - private ObservableCollection Source = new(); - private void btnAdd_Click(object sender, RoutedEventArgs e) + public ObservableCollection Args { - count++; - var r = new ArgTemplate { Arg = "", Count = count }; - Source.Add(r); - UpdateMainSource(); - view.SelectedItem = r; - } - public void UpdateSource() - { - Source.Clear(); - if (Args != null) - { - foreach (var item in Args) - { - count++; - var r = new ArgTemplate { Arg = item, Count = count }; - r.PropertyChanged += (_, _) => - { - UpdateMainSource(); - }; - Source.Add(r); - } - } - btnRemove.IsEnabled = Source.Any(); + get => (ObservableCollection)GetValue(ArgsProperty); + set => SetValue(ArgsProperty, value); } - private void UpdateMainSource() - { - Args.Clear(); - Args.AddRange(Source.Select(x=> x.Arg)); - } - private void btnRemove_Click(object sender, RoutedEventArgs e) - { - foreach (var item in view.SelectedItems) - { - Source.Remove((ArgTemplate)item); - } - UpdateMainSource(); - } + public static readonly DependencyProperty ArgsProperty = + DependencyProperty.Register( + nameof(Args), + typeof(ObservableCollection), + typeof(ArgumentsListView), + new PropertyMetadata(new ObservableCollection()) + ); - private void TextBox_PointerPressed(object sender, PointerRoutedEventArgs e) + public ArgumentsListView() { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); + InitializeComponent(); + view.ItemsSource = Args; } - private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + private void btnAdd_Click(object sender, RoutedEventArgs e) { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); - UpdateMainSource(); + Args.Add(string.Empty); + view.SelectedIndex = Args.Count - 1; } - private void TextBox_GotFocus(object sender, RoutedEventArgs e) + private void btnRemove_Click(object sender, RoutedEventArgs e) { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); + foreach (var selected in view.SelectedItems.Cast().ToList()) + Args.Remove(selected); } private void view_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml b/Emerald/UserControls/MinecraftSettingsUC.xaml new file mode 100644 index 00000000..cac1e5ef --- /dev/null +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml.cs b/Emerald/UserControls/MinecraftSettingsUC.xaml.cs new file mode 100644 index 00000000..be6f88d5 --- /dev/null +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; +using Emerald.CoreX.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage.Pickers; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Emerald.UserControls; +public sealed partial class MinecraftSettingsUC : UserControl +{ + public bool ShowMainPathEditor + { + get { return (bool)GetValue(ShowMainPathEditorProperty); } + set { SetValue(ShowMainPathEditorProperty, value); } + } + + // Using a DependencyProperty as the backing store for ShowMainPathEditor. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ShowMainPathEditorProperty = + DependencyProperty.Register("ShowMainPathEditor", typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + + public GameSettings GameSettings + { + get { return (GameSettings)GetValue(GameSettingsProperty); } + set { SetValue(GameSettingsProperty, value); } + } + + // Using a DependencyProperty as the backing store for GameSettings. This enables animation, styling, binding, etc... + public static readonly DependencyProperty GameSettingsProperty = + DependencyProperty.Register("GameSettings", typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + + + private readonly Services.SettingsService SS; + public MinecraftSettingsUC() + { + SS = Ioc.Default.GetService(); + this.InitializeComponent(); + } + + private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + { + this.Log().LogInformation("Choosing MC path"); + string path; + + var fop = new FolderPicker + { + CommitButtonText = "Select".Localize() + }; + fop.FileTypeFilter.Add("*"); + + if (DirectResoucres.Platform == "Windows") + WinRT.Interop.InitializeWithWindow.Initialize(fop, WinRT.Interop.WindowNative.GetWindowHandle(App.Current.MainWindow)); + + var f = await fop.PickSingleFolderAsync(); + + if (f != null) + path = f.Path; + else + { + this.Log().LogInformation("User did not select a MC path"); + return; + } + + this.Log().LogInformation("New Minecraft path: {path}", path); + SS.Settings.Minecraft.Path = path; + + await Ioc.Default.GetService().InitializeAndRefresh(new(path)); + } + + private void AdjustRam(int delta) + { + int newValue = GameSettings.MaximumRamMb + delta; + + GameSettings.MaximumRamMb = Math.Clamp( + newValue, + DirectResoucres.MinRAM, + DirectResoucres.MaxRAM + ); + } + + private void btnRamPlus_Click(object sender, RoutedEventArgs e) => + AdjustRam(64); + + private void btnRamMinus_Click(object sender, RoutedEventArgs e) => + AdjustRam(-64); + + private void btnAutoRAM_Click(object sender, RoutedEventArgs e) + { + int sysMax = DirectResoucres.MaxRAM; + + int recommended = sysMax switch + { + <= 4096 => DirectResoucres.MinRAM, // low-memory PCs + <= 8192 => sysMax / 3, // mid-range + <= 16384 => sysMax / 2, // standard gaming rigs + _ => (int)(sysMax * 0.65) // high RAM → ~65% + }; + + GameSettings.MaximumRamMb = recommended; + } + +} diff --git a/Emerald/Views/Settings/AppearancePage.xaml.cs b/Emerald/Views/Settings/AppearancePage.xaml.cs index 0d751b58..efc491ac 100644 --- a/Emerald/Views/Settings/AppearancePage.xaml.cs +++ b/Emerald/Views/Settings/AppearancePage.xaml.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices.WindowsRuntime; using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.Helpers; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/Emerald/Views/Settings/GeneralPage.xaml b/Emerald/Views/Settings/GeneralPage.xaml index 3803e660..9820b4e0 100644 --- a/Emerald/Views/Settings/GeneralPage.xaml +++ b/Emerald/Views/Settings/GeneralPage.xaml @@ -1,149 +1,7 @@  - - - - - - - - - - - - - - GB - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + mc:Ignorable="d" xmlns:helpers="using:Emerald.Helpers" + xmlns:cn="using:CommunityToolkit.WinUI.Controls" xmlns:uc="using:Emerald.UserControls"> + + diff --git a/Emerald/Views/Settings/GeneralPage.xaml.cs b/Emerald/Views/Settings/GeneralPage.xaml.cs index efbe333c..3aee34b9 100644 --- a/Emerald/Views/Settings/GeneralPage.xaml.cs +++ b/Emerald/Views/Settings/GeneralPage.xaml.cs @@ -34,46 +34,7 @@ public GeneralPage() { SS = Ioc.Default.GetService(); this.InitializeComponent(); + MCUC.GameSettings = SS.Settings.GameSettings; } - private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) - { - this.Log().LogInformation("Choosing MC path"); - string path; - - var fop = new FolderPicker - { - CommitButtonText = "Select".Localize() - }; - fop.FileTypeFilter.Add("*"); - - if(DirectResoucres.Platform == "Windows") - WinRT.Interop.InitializeWithWindow.Initialize(fop, WinRT.Interop.WindowNative.GetWindowHandle(App.Current.MainWindow)); - - var f = await fop.PickSingleFolderAsync(); - - if (f != null) - path = f.Path; - else - { - this.Log().LogInformation("User did not select a MC path"); - return; - } - - this.Log().LogInformation("New Minecraft path: {path}",path); - SS.Settings.Minecraft.Path = path; - - await Ioc.Default.GetService().InitializeAndRefresh(new(path)); - } - - private void btnRamPlus_Click(object sender, RoutedEventArgs e) => - SS.Settings.GameSettings.MaximumRamMb = SS.Settings.Minecraft.RAM + (SS.Settings.Minecraft.RAM >= DirectResoucres.MaxRAM ? 0 : (DirectResoucres.MaxRAM - SS.Settings.GameSettings.MaximumRamMb >= 50 ? 50 : DirectResoucres.MaxRAM - SS.Settings.GameSettings.MaximumRamMb)); - - - private void btnRamMinus_Click(object sender, RoutedEventArgs e) => - SS.Settings.Minecraft.RAM = SS.Settings.GameSettings.MaximumRamMb - (SS.Settings.GameSettings.MaximumRamMb <= DirectResoucres.MinRAM ? 0 : 50); - - - private void btnAutoRAM_Click(object sender, RoutedEventArgs e) => - SS.Settings.Minecraft.RAM = DirectResoucres.MaxRAM / 2; } diff --git a/global.json b/global.json index 8623b085..456efb61 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,8 @@ { "msbuild-sdks": { - "Uno.Sdk": "6.0.96" + "Uno.Sdk": "6.2.39" }, "sdk": { "allowPrerelease": false } -} \ No newline at end of file +} From a70388095c68bec87282f026668bc52b4d1fff7e Mon Sep 17 00:00:00 2001 From: NoobNotFound Date: Sat, 27 Sep 2025 20:50:05 +0530 Subject: [PATCH 05/26] Refactor localization keys to avoid a bug Replaces string-based argument handling with a LaunchArg model and synchronizes external/internal collections in ArgumentsListView. Updates all localization markup extensions to use KeyName instead of Name. Refactors MinecraftSettingsUC to use ShowMainSettings and adds copy path functionality. Updates target frameworks to net9.0-desktop and net9.0-windows10.0.26100. Cleans up code and improves bindings and UI logic across settings and controls. --- .vscode/launch.json | 3 +- .vscode/tasks.json | 4 +- Emerald.CoreX/Emerald.CoreX.csproj | 1 - Emerald/Emerald.csproj | 3 +- .../MarkupExtensions/LocalizeString.cs | 4 +- Emerald/Models/ArgTemplate.cs | 9 +- Emerald/UserControls/ArgumentsListView.xaml | 32 +++--- .../UserControls/ArgumentsListView.xaml.cs | 100 ++++++++++++++---- Emerald/UserControls/MinecraftSettingsUC.xaml | 89 +++++++++------- .../UserControls/MinecraftSettingsUC.xaml.cs | 93 ++++++++-------- .../UserControls/NotificationListControl.xaml | 12 +-- Emerald/Views/Settings/AppearancePage.xaml | 38 +++---- Emerald/Views/Settings/GeneralPage.xaml | 2 +- Emerald/Views/Settings/SettingsPage.xaml | 6 +- 14 files changed, 235 insertions(+), 161 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1d6e6504..5c5560ed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ + { "name": "Uno Platform Mobile", "type": "Uno", @@ -20,7 +21,7 @@ "request": "launch", "preLaunchTask": "build-desktop", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/Emerald/bin/Debug/net8.0-desktop/Emerald.dll", + "program": "${workspaceFolder}/Emerald/bin/Debug/net9.0-desktop/Emerald.dll", "args": [], "launchSettingsProfile": "Emerald (Desktop)", "env": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ac5be062..7e63da46 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,7 @@ "build", "${workspaceFolder}/Emerald/Emerald.csproj", "/property:GenerateFullPaths=true", - "/property:TargetFramework=net8.0-desktop", + "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" @@ -22,7 +22,7 @@ "publish", "${workspaceFolder}/Emerald/Emerald.csproj", "/property:GenerateFullPaths=true", - "/property:TargetFramework=net8.0-desktop", + "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" diff --git a/Emerald.CoreX/Emerald.CoreX.csproj b/Emerald.CoreX/Emerald.CoreX.csproj index 020b44f6..3b7a82c1 100644 --- a/Emerald.CoreX/Emerald.CoreX.csproj +++ b/Emerald.CoreX/Emerald.CoreX.csproj @@ -5,7 +5,6 @@ net9.0-desktop; $(TargetFrameworks);net9.0-windows10.0.22621 - $(TargetFrameworks);net9.0-maccatalyst true Library diff --git a/Emerald/Emerald.csproj b/Emerald/Emerald.csproj index 2d43a499..85b99e2a 100644 --- a/Emerald/Emerald.csproj +++ b/Emerald/Emerald.csproj @@ -3,8 +3,7 @@ net9.0-desktop; - $(TargetFrameworks);net9.0-windows10.0.22621 - $(TargetFrameworks);net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.26100 Exe true diff --git a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs index e45bca8b..8812d857 100644 --- a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs +++ b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs @@ -6,8 +6,8 @@ namespace Emerald.Helpers; [MarkupExtensionReturnType(ReturnType = typeof(string))] public sealed class Localize : MarkupExtension { - public string Name { get; set; } + public string KeyName { get; set; } protected override object ProvideValue() - => Name.Localize(); + => KeyName.Localize(); } diff --git a/Emerald/Models/ArgTemplate.cs b/Emerald/Models/ArgTemplate.cs index ee708800..f43a5e2d 100644 --- a/Emerald/Models/ArgTemplate.cs +++ b/Emerald/Models/ArgTemplate.cs @@ -1,13 +1,8 @@ -using CommunityToolkit.Mvvm.ComponentModel; - namespace Emerald.Models; -//Copied from Emerald.UWP [ObservableObject] -public partial class ArgTemplate +public partial class LaunchArg { [ObservableProperty] - private string arg; - - public int Count { get; set; } + public string value = string.Empty; } diff --git a/Emerald/UserControls/ArgumentsListView.xaml b/Emerald/UserControls/ArgumentsListView.xaml index 31249d27..c0022268 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml +++ b/Emerald/UserControls/ArgumentsListView.xaml @@ -1,12 +1,13 @@  + x:Class="Emerald.UserControls.ArgumentsListView" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="using:Emerald.Models" + mc:Ignorable="d" + d:DesignHeight="300" + d:DesignWidth="400"> @@ -27,14 +28,15 @@ - + + Margin="-9,-4" + VerticalAlignment="Stretch" + Background="Transparent" + BorderThickness="0" + PlaceholderText="Empty" + Text="{x:Bind Value, Mode=TwoWay}" + GotFocus="TextBox_GotFocus" /> diff --git a/Emerald/UserControls/ArgumentsListView.xaml.cs b/Emerald/UserControls/ArgumentsListView.xaml.cs index 13386def..077f2e02 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml.cs +++ b/Emerald/UserControls/ArgumentsListView.xaml.cs @@ -1,25 +1,16 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; +using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using CommunityToolkit.Mvvm.DependencyInjection; -using Emerald.CoreX.Helpers; -using Emerald.CoreX.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.Storage.Pickers; +using Emerald.Models; + namespace Emerald.UserControls; + public sealed partial class ArgumentsListView : UserControl { + // Public API → strings public ObservableCollection Args { get => (ObservableCollection)GetValue(ArgsProperty); @@ -31,29 +22,96 @@ public ObservableCollection Args nameof(Args), typeof(ObservableCollection), typeof(ArgumentsListView), - new PropertyMetadata(new ObservableCollection()) + new PropertyMetadata(new ObservableCollection(), OnArgsChanged) ); + // Internal collection for binding + private readonly ObservableCollection _internal = new(); + public ArgumentsListView() { InitializeComponent(); - view.ItemsSource = Args; + view.ItemsSource = _internal; + } + + private static void OnArgsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (ArgumentsListView)d; + + if (e.OldValue is ObservableCollection oldCol) + oldCol.CollectionChanged -= control.ExternalChanged; + + if (e.NewValue is ObservableCollection newCol) + { + newCol.CollectionChanged += control.ExternalChanged; + control.SyncFromExternal(); + } + } + + // Sync external → internal + private void SyncFromExternal() + { + _internal.Clear(); + + if (Args == null) return; + + foreach (var s in Args) + { + var arg = new LaunchArg { Value = s }; + arg.PropertyChanged += InternalArgChanged; + _internal.Add(arg); + } } + // Sync internal → external + private void SyncToExternal() + { + if (Args == null) return; + + Args.CollectionChanged -= ExternalChanged; + Args.Clear(); + foreach (var arg in _internal) + Args.Add(arg.Value); + Args.CollectionChanged += ExternalChanged; + } + + private void InternalArgChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LaunchArg.Value)) + SyncToExternal(); + } + + private void ExternalChanged(object? sender, NotifyCollectionChangedEventArgs e) => + SyncFromExternal(); + private void btnAdd_Click(object sender, RoutedEventArgs e) { - Args.Add(string.Empty); - view.SelectedIndex = Args.Count - 1; + var newArg = new LaunchArg { Value = string.Empty }; + newArg.PropertyChanged += InternalArgChanged; + _internal.Add(newArg); + view.SelectedIndex = _internal.Count - 1; + SyncToExternal(); } private void btnRemove_Click(object sender, RoutedEventArgs e) { - foreach (var selected in view.SelectedItems.Cast().ToList()) - Args.Remove(selected); + foreach (var selected in view.SelectedItems.Cast().ToList()) + { + selected.PropertyChanged -= InternalArgChanged; + _internal.Remove(selected); + } + + SyncToExternal(); } private void view_SelectionChanged(object sender, SelectionChangedEventArgs e) { btnRemove.IsEnabled = view.SelectedItems.Any(); } + private void TextBox_GotFocus(object sender, RoutedEventArgs e) + { + if (sender is TextBox tb && tb.DataContext is LaunchArg arg) + view.SelectedItem = arg; + } + } diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml b/Emerald/UserControls/MinecraftSettingsUC.xaml index cac1e5ef..b2c455ad 100644 --- a/Emerald/UserControls/MinecraftSettingsUC.xaml +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml @@ -7,33 +7,46 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:helpers="using:Emerald.Helpers" xmlns:cn="using:CommunityToolkit.WinUI.Controls" + xmlns:conv="using:Emerald.Helpers.Converters" d:DesignHeight="300" d:DesignWidth="400"> + + + + + - + - + Header="{helpers:Localize KeyName=MinecraftPath}" + Description="{helpers:Localize KeyName=MinecraftPathDescription}"> + - + + + + + + + + Header="{helpers:Localize KeyName=RAM}" + Description="{helpers:Localize KeyName=RAMDescription}"> @@ -56,17 +69,17 @@ + Description="{helpers:Localize KeyName=DownloadSettingsDescription}"> + Header="{helpers:Localize KeyName=AssetsCheck}"> + Header="{helpers:Localize KeyName=HashCheck}"> @@ -74,44 +87,44 @@ + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + Text="{helpers:Localize KeyName=AdvancedMinecraftSettings}" /> + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + Header="{helpers:Localize KeyName=Width}"> + PlaceholderText="{helpers:Localize KeyName=Default}" /> + Header="{helpers:Localize KeyName=Height}"> + PlaceholderText="{helpers:Localize KeyName=Default}" /> + Header="{helpers:Localize KeyName=FullScreen}"> (bool)GetValue(ShowMainSettingsProperty); + set => SetValue(ShowMainSettingsProperty, value); } - // Using a DependencyProperty as the backing store for ShowMainPathEditor. This enables animation, styling, binding, etc... - public static readonly DependencyProperty ShowMainPathEditorProperty = - DependencyProperty.Register("ShowMainPathEditor", typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + public static readonly DependencyProperty ShowMainSettingsProperty = + DependencyProperty.Register(nameof(ShowMainSettings), typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(false)); public GameSettings GameSettings { - get { return (GameSettings)GetValue(GameSettingsProperty); } - set { SetValue(GameSettingsProperty, value); } + get => (GameSettings)GetValue(GameSettingsProperty); + set => SetValue(GameSettingsProperty, value); } - // Using a DependencyProperty as the backing store for GameSettings. This enables animation, styling, binding, etc... public static readonly DependencyProperty GameSettingsProperty = - DependencyProperty.Register("GameSettings", typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(GameSettings), typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + // expose SS as a public property if you bind to it from x:Bind in XAML, otherwise x:Bind may not resolve. + public Services.SettingsService SS { get; } - private readonly Services.SettingsService SS; public MinecraftSettingsUC() { + InitializeComponent(); SS = Ioc.Default.GetService(); - this.InitializeComponent(); } - private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + // helper method so we can call from multiple handlers + private async Task PickMinecraftFolderAsync() { this.Log().LogInformation("Choosing MC path"); - string path; - var fop = new FolderPicker - { - CommitButtonText = "Select".Localize() - }; + var fop = new FolderPicker { CommitButtonText = "Select".Localize() }; fop.FileTypeFilter.Add("*"); if (DirectResoucres.Platform == "Windows") @@ -66,20 +52,45 @@ private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) var f = await fop.PickSingleFolderAsync(); - if (f != null) - path = f.Path; - else + if (f == null) { this.Log().LogInformation("User did not select a MC path"); return; } + var path = f.Path; this.Log().LogInformation("New Minecraft path: {path}", path); SS.Settings.Minecraft.Path = path; await Ioc.Default.GetService().InitializeAndRefresh(new(path)); } + private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + { + await PickMinecraftFolderAsync(); + } + + private async void ChangePath_OnClick(object sender, RoutedEventArgs e) + { + // call the same helper instead of calling the handler with nulls + await PickMinecraftFolderAsync(); + } + + private void CopyPath_OnClick(object sender, RoutedEventArgs e) + { + try + { + var path = ShowMainSettings ? SS.Settings.Minecraft.Path : Path.Combine(SS.Settings.Minecraft.Path, CoreX.Core.GamesFolderName); + var dp = new DataPackage(); + dp.SetText(path); + Clipboard.SetContent(dp); + } + catch (Exception ex) + { + this.Log().LogError(ex, "Failed to copy path"); + } + } + private void AdjustRam(int delta) { int newValue = GameSettings.MaximumRamMb + delta; @@ -91,11 +102,8 @@ private void AdjustRam(int delta) ); } - private void btnRamPlus_Click(object sender, RoutedEventArgs e) => - AdjustRam(64); - - private void btnRamMinus_Click(object sender, RoutedEventArgs e) => - AdjustRam(-64); + private void btnRamPlus_Click(object sender, RoutedEventArgs e) => AdjustRam(64); + private void btnRamMinus_Click(object sender, RoutedEventArgs e) => AdjustRam(-64); private void btnAutoRAM_Click(object sender, RoutedEventArgs e) { @@ -103,13 +111,12 @@ private void btnAutoRAM_Click(object sender, RoutedEventArgs e) int recommended = sysMax switch { - <= 4096 => DirectResoucres.MinRAM, // low-memory PCs - <= 8192 => sysMax / 3, // mid-range - <= 16384 => sysMax / 2, // standard gaming rigs - _ => (int)(sysMax * 0.65) // high RAM → ~65% + <= 4096 => DirectResoucres.MinRAM, + <= 8192 => sysMax / 3, + <= 16384 => sysMax / 2, + _ => (int)(sysMax * 0.65) }; GameSettings.MaximumRamMb = recommended; } - } diff --git a/Emerald/UserControls/NotificationListControl.xaml b/Emerald/UserControls/NotificationListControl.xaml index 4604c9d1..3288540b 100644 --- a/Emerald/UserControls/NotificationListControl.xaml +++ b/Emerald/UserControls/NotificationListControl.xaml @@ -40,7 +40,7 @@ - @@ -63,8 +63,8 @@ -