From 9800da36a249e7f6a97a7376eaf87016226134bd Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Sun, 4 Jun 2023 19:14:55 +0100 Subject: [PATCH 01/25] =?UTF-8?q?Let=E2=80=99s=20start=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 3 + .../UI/Controls/ApplicationContextMenu.axaml | 4 + .../Controls/ApplicationContextMenu.axaml.cs | 10 + src/Ryujinx.Ava/UI/Models/ModModel.cs | 31 +++ .../UI/ViewModels/ModManagerViewModel.cs | 124 ++++++++++++ .../UI/Windows/ModManagerWindow.axaml | 180 ++++++++++++++++++ .../UI/Windows/ModManagerWindow.axaml.cs | 115 +++++++++++ 7 files changed, 467 insertions(+) create mode 100644 src/Ryujinx.Ava/UI/Models/ModModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 72b5e8e3c..40fbb6eec 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -506,6 +506,8 @@ "EnableInternetAccessTooltip": "Allows the emulated application to connect to the Internet.\n\nGames with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well.\n\nDoes NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet.\n\nLeave OFF if unsure.", "GameListContextMenuManageCheatToolTip": "Manage Cheats", "GameListContextMenuManageCheat": "Manage Cheats", + "GameListContextMenuManageModToolTip": "Manage Mods", + "GameListContextMenuManageMod": "Manage Mods", "ControllerSettingsStickRange": "Range:", "DialogStopEmulationTitle": "Ryujinx - Stop Emulation", "DialogStopEmulationMessage": "Are you sure you want to stop emulation?", @@ -597,6 +599,7 @@ "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowHeading": "{0} Downloadable Content(s)", + "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", "Save": "Save", diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index b8fe7e76f..46c37ef2a 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -46,6 +46,10 @@ Click="OpenCheatManager_Click" Header="{locale:Locale GameListContextMenuManageCheat}" ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" /> + _enabled; + set + { + _enabled = value; + OnPropertyChanged(); + } + } + + public string ContainerPath { get; } + public string FullPath { get; } + public string FileName => Path.GetFileName(ContainerPath); + + public ModModel(string containerPath, string fullPath, bool enabled) + { + ContainerPath = containerPath; + FullPath = fullPath; + Enabled = enabled; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs new file mode 100644 index 000000000..2ad3ddd64 --- /dev/null +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -0,0 +1,124 @@ +using Avalonia.Collections; +using DynamicData; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ModManagerViewModel : BaseModel + { + public AvaloniaList _mods = new(); + public AvaloniaList _views = new(); + public AvaloniaList _selectedMods = new(); + + private string _search; + + public AvaloniaList Mods + { + get => _mods; + set + { + _mods = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + } + + public AvaloniaList Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public AvaloniaList SelectedMods + { + get => _selectedMods; + set + { + _selectedMods = value; + OnPropertyChanged(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public string ModCount + { + get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); + } + + public ModManagerViewModel() + { + LoadMods(); + } + + private void LoadMods() + { + + } + + public void Sort() + { + Mods.AsObservableChangeSet() + .Filter(Filter) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + OnPropertyChanged(nameof(ModCount)); + } + + private bool Filter(object arg) + { + if (arg is ModModel content) + { + return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + public void Save() + { + + } + + public void Remove(ModModel model) + { + Mods.Remove(model); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + + public void RemoveAll() + { + Mods.Clear(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + + public void EnableAll() + { + SelectedMods = new(Mods); + } + + public void DisableAll() + { + SelectedMods.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml new file mode 100644 index 000000000..d98cf0bde --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs new file mode 100644 index 000000000..3e64c2c96 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -0,0 +1,115 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ModManagerWindow : UserControl + { + public ModManagerViewModel ViewModel; + + public ModManagerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public ModManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + DataContext = ViewModel = new ModManagerViewModel(); + + InitializeComponent(); + } + + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new ModManagerWindow(virtualFileSystem, titleId, titleName), + Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], titleName, titleId.ToString("X16")) + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void SaveAndClose(object sender, RoutedEventArgs e) + { + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void RemoveMod(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + ViewModel.Remove(model); + } + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + OpenHelper.LocateFile(model.ContainerPath); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = true; + } + } + } + + foreach (var content in e.RemovedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = false; + } + } + } + } + } +} \ No newline at end of file From fe74d3b773f0b0dc29c909835627d1f93956cb96 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Sun, 4 Jun 2023 22:08:30 +0100 Subject: [PATCH 02/25] Read folders and such --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/UI/Models/ModModel.cs | 11 +- .../UI/ViewModels/ModManagerViewModel.cs | 127 +++++++++++++++++- .../UI/Windows/ModManagerWindow.axaml | 2 +- .../UI/Windows/ModManagerWindow.axaml.cs | 4 +- src/Ryujinx.Common/Configuration/Mod.cs | 8 ++ .../Configuration/ModMetadata.cs | 9 ++ .../ModMetadataJsonSerializerContext.cs | 10 ++ 8 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 src/Ryujinx.Common/Configuration/Mod.cs create mode 100644 src/Ryujinx.Common/Configuration/ModMetadata.cs create mode 100644 src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 40fbb6eec..41ff5b1ec 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -388,6 +388,7 @@ "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", "DialogLoadNcaErrorMessage": "{0}. Errored File: {1}", + "DialogLoadModErrorMessage": "{0}. Errored File: {1}", "DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!", "DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.", "DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?", @@ -592,6 +593,7 @@ "Writable": "Writable", "SelectDlcDialogTitle": "Select DLC files", "SelectUpdateDialogTitle": "Select update files", + "SelectModDialogTitle": "Select mod directory", "UserProfileWindowTitle": "User Profiles Manager", "CheatWindowTitle": "Cheats Manager", "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", diff --git a/src/Ryujinx.Ava/UI/Models/ModModel.cs b/src/Ryujinx.Ava/UI/Models/ModModel.cs index 35b035b7f..4b3394dd4 100644 --- a/src/Ryujinx.Ava/UI/Models/ModModel.cs +++ b/src/Ryujinx.Ava/UI/Models/ModModel.cs @@ -17,14 +17,13 @@ public bool Enabled } } - public string ContainerPath { get; } - public string FullPath { get; } - public string FileName => Path.GetFileName(ContainerPath); + public string Path { get; } + public string Name { get; } - public ModModel(string containerPath, string fullPath, bool enabled) + public ModModel(string path, string name, bool enabled) { - ContainerPath = containerPath; - FullPath = fullPath; + Path = path; + Name = name; Enabled = enabled; } } diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 2ad3ddd64..44c4a564c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -1,18 +1,38 @@ using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using DynamicData; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { + public ModMetadata _modData; + public readonly string _modJsonPath; + public AvaloniaList _mods = new(); public AvaloniaList _views = new(); public AvaloniaList _selectedMods = new(); + private ulong _titleId { get; } + private string _titleName { get; } + private string _search; + private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public AvaloniaList Mods { get => _mods; @@ -61,14 +81,64 @@ public string ModCount get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); } - public ModManagerViewModel() + public ModManagerViewModel(ulong titleId, string titleName) { - LoadMods(); + _titleId = titleId; + _titleName = titleName; + + _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + + try + { + _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize mod data for {_titleId} at {_modJsonPath}"); + + _modData = new ModMetadata + { + Mods = new List() + }; + + Save(); + } + + LoadMods(titleId); } - private void LoadMods() + private void LoadMods(ulong titleId) { + string modsBasePath = ModLoader.GetSdModsBasePath(); + + var modCache = new ModLoader.ModCache(); + ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); + + foreach (var mod in modCache.RomfsDirs) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.RomfsContainers) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.ExefsDirs) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.ExefsContainers) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + Sort(); } public void Sort() @@ -86,7 +156,7 @@ private bool Filter(object arg) { if (arg is ModModel content) { - return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()); + return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower()); } return false; @@ -94,7 +164,18 @@ private bool Filter(object arg) public void Save() { + _modData.Mods.Clear(); + foreach (ModModel mod in SelectedMods) + { + _modData.Mods.Add(new Mod + { + Name = mod.Name, + Path = mod.Path, + }); + } + + JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); } public void Remove(ModModel model) @@ -104,6 +185,44 @@ public void Remove(ModModel model) Sort(); } + private void AddMod(string directory) + { + if (Directory.Exists(directory) && Mods.All(x => !x.Path.Contains(directory))) + { + using FileStream file = new(directory, FileMode.Open, FileAccess.Read); + + try + { + + } + catch (Exception ex) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, ex.Message, directory)); + }); + } + } + } + + public async void Add() + { + OpenFolderDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] + }; + + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + string directory = await dialog.ShowAsync(desktop.MainWindow); + + if (directory != null) + { + AddMod(directory); + } + } + } + public void RemoveAll() { Mods.Clear(); diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index d98cf0bde..ad054ed52 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -92,7 +92,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding FileName}" /> + Text="{Binding Name}" /> Mods { get; set; } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs new file mode 100644 index 000000000..f1cd2cf21 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ModMetadata))] + public partial class ModMetadataJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file From 98b3cd1fe181287ebdfd0dabb1f1ea43ca4cc326 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 00:33:45 +0100 Subject: [PATCH 03/25] Remove Open Mod Folder menu items --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 4 --- .../UI/Controls/ApplicationContextMenu.axaml | 8 ------ .../Controls/ApplicationContextMenu.axaml.cs | 26 ------------------- 3 files changed, 38 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 41ff5b1ec..33c33299f 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -54,8 +54,6 @@ "GameListContextMenuManageTitleUpdatesToolTip": "Opens the Title Update management window", "GameListContextMenuManageDlc": "Manage DLC", "GameListContextMenuManageDlcToolTip": "Opens the DLC management window", - "GameListContextMenuOpenModsDirectory": "Open Mods Directory", - "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuCacheManagement": "Cache Management", "GameListContextMenuCacheManagementPurgePptc": "Queue PPTC Rebuild", "GameListContextMenuCacheManagementPurgePptcToolTip": "Trigger PPTC to rebuild at boot time on the next game launch", @@ -520,8 +518,6 @@ "SettingsTabCpuMemory": "CPU Mode", "DialogUpdaterFlatpakNotSupportedMessage": "Please update Ryujinx via FlatHub.", "UpdaterDisabledWarningTitle": "Updater Disabled!", - "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", - "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", "ControllerSettingsRotate90": "Rotate 90° Clockwise", "IconSize": "Icon Size", "IconSizeTooltip": "Change the size of game icons", diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index 46c37ef2a..7f786cf38 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -50,14 +50,6 @@ Click="OpenModManager_Click" Header="{locale:Locale GameListContextMenuManageMod}" ToolTip.Tip="{locale:Locale GameListContextMenuManageModToolTip}" /> - - Date: Mon, 5 Jun 2023 00:44:52 +0100 Subject: [PATCH 04/25] Fix folder opening, Selecting/deselecting --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 2 ++ src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 44c4a564c..364b565c9 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -138,6 +138,8 @@ private void LoadMods(ulong titleId) Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); } + SelectedMods = new(Mods.Where(x => x.Enabled)); + Sort(); } diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 18f25f997..bd8c9f4c3 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -78,7 +78,7 @@ private void OpenLocation(object sender, RoutedEventArgs e) { if (button.DataContext is ModModel model) { - OpenHelper.LocateFile(model.Path); + OpenHelper.OpenFolder(model.Path); } } } From 99b7fe27c58a50e8eebac92b90c06a6da9814395 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 14:02:24 +0100 Subject: [PATCH 05/25] She works --- .../UI/ViewModels/ModManagerViewModel.cs | 14 +-- .../Configuration/AppDataManager.cs | 4 +- src/Ryujinx.HLE/HOS/ModLoader.cs | 103 +++++++++++++----- .../Extensions/LocalFileSystemExtensions.cs | 5 +- .../Processes/Extensions/NcaExtensions.cs | 4 +- 5 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 364b565c9..431f14dbc 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -109,33 +109,29 @@ public ModManagerViewModel(ulong titleId, string titleName) private void LoadMods(ulong titleId) { - string modsBasePath = ModLoader.GetSdModsBasePath(); + string modsBasePath = ModLoader.GetModsBasePath(); var modCache = new ModLoader.ModCache(); ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); foreach (var mod in modCache.RomfsDirs) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.RomfsContainers) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.ExefsDirs) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.ExefsContainers) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } SelectedMods = new(Mods.Where(x => x.Enabled)); diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 2b4a594d3..350a37a98 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -31,7 +31,6 @@ public enum LaunchMode public const string DefaultNandDir = "bis"; public const string DefaultSdcardDir = "sdcard"; - private const string DefaultModsDir = "mods"; public static string CustomModsPath { get; set; } public static string CustomSdModsPath { get; set; } @@ -151,7 +150,6 @@ private static void CopyDirectory(string sourceDir, string destinationDir) } } - public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName; - public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; + public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 834bc0595..d2e945880 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -7,6 +7,7 @@ using LibHac.Tools.FsSystem.RomFs; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Mods; @@ -37,15 +38,19 @@ public class ModLoader private const string AmsNroPatchDir = "nro_patches"; private const string AmsKipPatchDir = "kip_patches"; + private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public readonly struct Mod where T : FileSystemInfo { public readonly string Name; public readonly T Path; + public readonly bool Enabled; - public Mod(string name, T path) + public Mod(string name, T path, bool enabled) { Name = name; Path = path; + Enabled = enabled; } } @@ -138,7 +143,6 @@ private void Clear() private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); - public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); private static string EnsureBaseDirStructure(string modsBasePath) { @@ -160,19 +164,35 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { System.Text.StringBuilder types = new(); + string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); + ModMetadata? ModData = null; + + try + { + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + } + foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null); + Mod mod = new("", null, true); if (StrEquals(RomfsDir, modDir.Name)) { - mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + + mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + + mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); } else if (StrEquals(CheatDir, modDir.Name)) @@ -238,28 +258,44 @@ private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir) foreach (var modDir in patchDir.EnumerateDirectories()) { - patches.Add(new Mod(modDir.Name, modDir)); + patches.Add(new Mod(modDir.Name, modDir, true)); Logger.Info?.Print(LogClass.ModLoader, $"Found {type} patch '{modDir.Name}'"); } } - private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) + private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong titleId) { if (!titleDir.Exists) { return; } + string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + ModMetadata? ModData = null; + + try + { + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + } + var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + + mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + + mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } AddModsFromDirectory(mods, titleDir, titleDir.Name); @@ -278,7 +314,7 @@ public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ul if (titleDir != null) { - QueryTitleDir(mods, titleDir); + QueryTitleDir(mods, titleDir, titleId); } } @@ -375,7 +411,7 @@ private static IEnumerable GetCheatsInFile(FileInfo cheatFile) } // Assumes searchDirPaths don't overlap - private static void CollectMods(Dictionary modCaches, PatchCache patches, params string[] searchDirPaths) + private static void CollectMods(Dictionary modCaches, PatchCache patches) { static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || StrEquals(AmsNroPatchDir, name) || @@ -404,28 +440,25 @@ static bool TryQuery(DirectoryInfo searchDir, PatchCache patches, Dictionary titles, params string[] searchDirPaths) + public void CollectMods(IEnumerable titles) { Clear(); @@ -434,7 +467,7 @@ public void CollectMods(IEnumerable titles, params string[] searchDirPath _appMods[titleId] = new ModCache(); } - CollectMods(_appMods, _patches, searchDirPaths); + CollectMods(_appMods, _patches); } internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) @@ -453,6 +486,11 @@ internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) // Prioritize loose files first foreach (var mod in mods.RomfsDirs) { + if (!mod.Enabled) + { + continue; + } + using (IFileSystem fs = new LocalFileSystem(mod.Path.FullName)) { AddFiles(fs, mod.Name, fileSet, builder); @@ -463,6 +501,11 @@ internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) // Then files inside images foreach (var mod in mods.RomfsContainers) { + if (!mod.Enabled) + { + continue; + } + Logger.Info?.Print(LogClass.ModLoader, $"Found 'romfs.bin' for Title {titleId:X16}"); using (IFileSystem fs = new RomFsFileSystem(mod.Path.OpenRead().AsStorage())) { @@ -571,6 +614,11 @@ internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos) foreach (var mod in exeMods) { + if (!mod.Enabled) + { + continue; + } + for (int i = 0; i < ProcessConst.ExeFsPrefixes.Length; ++i) { var nsoName = ProcessConst.ExeFsPrefixes[i]; @@ -724,6 +772,11 @@ private static bool ApplyProgramPatches(IEnumerable> mods, in // Collect patches foreach (var mod in mods) { + if (!mod.Enabled) + { + continue; + } + var patchDir = mod.Path; foreach (var patchFile in patchDir.EnumerateFiles()) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs index 798a9261e..831c33ea6 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs @@ -16,10 +16,7 @@ public static ProcessResult Load(this LocalFileSystem exeFs, Switch device, stri var nacpData = new BlitStruct(1); ulong programId = metaLoader.GetProgramId(); - device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - new[] { programId }, - ModLoader.GetModsBasePath(), - ModLoader.GetSdModsBasePath()); + device.Configuration.VirtualFileSystem.ModLoader.CollectMods(new[] { programId }); if (programId != 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index e369f4b04..de396ae9d 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -35,9 +35,7 @@ public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca // Collecting mods related to AocTitleIds and ProgramId. device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), - ModLoader.GetModsBasePath(), - ModLoader.GetSdModsBasePath()); + device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId())); // Load Nacp file. var nacpData = new BlitStruct(1); From a4b69c5fb883e14ef9e970ca906bed2639c8d7ee Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 14:06:01 +0100 Subject: [PATCH 06/25] Fix GTK --- .../Ui/Widgets/GameTableContextMenu.Designer.cs | 11 ----------- src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 734437eea..370d22640 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -11,7 +11,6 @@ public partial class GameTableContextMenu : Menu private MenuItem _manageDlcMenuItem; private MenuItem _manageCheatMenuItem; private MenuItem _openTitleModDirMenuItem; - private MenuItem _openTitleSdModDirMenuItem; private Menu _extractSubMenu; private MenuItem _extractMenuItem; private MenuItem _extractRomFsMenuItem; @@ -90,15 +89,6 @@ private void InitializeComponent() }; _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; - // - // _openTitleSdModDirMenuItem - // - _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory") - { - TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.", - }; - _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked; - // // _extractSubMenu // @@ -221,7 +211,6 @@ private void ShowComponent() Add(_manageDlcMenuItem); Add(_manageCheatMenuItem); Add(_openTitleModDirMenuItem); - Add(_openTitleSdModDirMenuItem); Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index eb048b00d..425766b5a 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -496,15 +496,7 @@ private void ManageCheats_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args) { - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); - - OpenHelper.OpenFolder(titleModsPath); - } - - private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) - { - string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string sdModsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); From e8d523b8b56af10d115563e24144ac5a34dd476c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 16:17:08 +0100 Subject: [PATCH 07/25] AddMod --- .../UI/ViewModels/ModManagerViewModel.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 431f14dbc..691252dde 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -183,24 +183,37 @@ public void Remove(ModModel model) Sort(); } - private void AddMod(string directory) + private void AddMod(DirectoryInfo directory) { - if (Directory.Exists(directory) && Mods.All(x => !x.Path.Contains(directory))) - { - using FileStream file = new(directory, FileMode.Open, FileAccess.Read); + var directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories); + var destinationDir = ModLoader.GetTitleDir(ModLoader.GetModsBasePath(), _titleId.ToString("x16")); - try - { + foreach (var dir in directories) + { + string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir); - } - catch (Exception ex) + // Mod already exists + if (Directory.Exists(dirToCreate)) { Dispatcher.UIThread.Post(async () => { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, ex.Message, directory)); + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, "Director", dirToCreate)); }); + + return; } + + Directory.CreateDirectory(dirToCreate); } + + var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories); + + foreach (var file in files) + { + File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true); + } + + LoadMods(_titleId); } public async void Add() @@ -216,7 +229,7 @@ public async void Add() if (directory != null) { - AddMod(directory); + AddMod(new DirectoryInfo(directory)); } } } From 83ef3315a3558f6c6d63114f0be4ffc4fd48f0f6 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 16:23:00 +0100 Subject: [PATCH 08/25] Delete --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 1 + .../UI/ViewModels/ModManagerViewModel.cs | 13 +++++++++++-- src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml | 6 +++--- .../UI/Windows/ModManagerWindow.axaml.cs | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 33c33299f..07c843142 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -431,6 +431,7 @@ "DlcManagerRemoveAllButton": "Remove All", "DlcManagerEnableAllButton": "Enable All", "DlcManagerDisableAllButton": "Disable All", + "ModManagerDeleteAllButton": "Delete All", "MenuBarOptionsChangeLanguage": "Change Language", "MenuBarShowFileTypes": "Show File Types", "CommonSort": "Sort", diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 691252dde..560aa33f6 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -109,6 +109,8 @@ public ModManagerViewModel(ulong titleId, string titleName) private void LoadMods(ulong titleId) { + Mods.Clear(); + string modsBasePath = ModLoader.GetModsBasePath(); var modCache = new ModLoader.ModCache(); @@ -176,8 +178,10 @@ public void Save() JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); } - public void Remove(ModModel model) + public void Delete(ModModel model) { + Directory.Delete(model.Path, true); + Mods.Remove(model); OnPropertyChanged(nameof(ModCount)); Sort(); @@ -234,8 +238,13 @@ public async void Add() } } - public void RemoveAll() + public void DeleteAll() { + foreach (var mod in Mods) + { + Directory.Delete(mod.Path, true); + } + Mods.Clear(); OnPropertyChanged(nameof(ModCount)); Sort(); diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index ad054ed52..d56d43ea3 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -116,7 +116,7 @@ Padding="10" MinWidth="0" MinHeight="0" - Click="RemoveMod"> + Click="DeleteMod"> - + Command="{ReflectionBinding DeleteAll}"> + Date: Tue, 27 Jun 2023 01:03:16 +0100 Subject: [PATCH 09/25] Fix duplicate entries --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 560aa33f6..489d6d3a8 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -118,7 +118,11 @@ private void LoadMods(ulong titleId) foreach (var mod in modCache.RomfsDirs) { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } foreach (var mod in modCache.RomfsContainers) @@ -128,7 +132,11 @@ private void LoadMods(ulong titleId) foreach (var mod in modCache.ExefsDirs) { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } foreach (var mod in modCache.ExefsContainers) From 4b64616932f9ef51ea4e397c0bcd69c1bad0fb0d Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Tue, 27 Jun 2023 01:06:26 +0100 Subject: [PATCH 10/25] Fix file check --- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index d2e945880..b67302470 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -285,7 +285,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } @@ -293,7 +293,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } From b14f8080f604ef67d85bbf6ebb100117cb76c4dd Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 12:53:58 +0100 Subject: [PATCH 11/25] Avalonia 11 --- .../Controls/ApplicationContextMenu.axaml.cs | 2 +- src/Ryujinx.Ava/UI/Models/ModModel.cs | 2 +- .../DownloadableContentManagerViewModel.cs | 7 ++-- .../UI/ViewModels/ModManagerViewModel.cs | 32 +++++++++---------- .../UI/Windows/ModManagerWindow.axaml | 9 +++--- .../UI/Windows/ModManagerWindow.axaml.cs | 10 +++--- src/Ryujinx.Common/Configuration/Mod.cs | 2 +- .../Configuration/ModMetadata.cs | 2 +- .../ModMetadataJsonSerializerContext.cs | 2 +- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 81c087d27..01d977091 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -132,7 +132,7 @@ public async void OpenModManager_Click(object sender, RoutedEventArgs args) if (viewModel?.SelectedApplication != null) { - await ModManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } diff --git a/src/Ryujinx.Ava/UI/Models/ModModel.cs b/src/Ryujinx.Ava/UI/Models/ModModel.cs index 4b3394dd4..f68e15939 100644 --- a/src/Ryujinx.Ava/UI/Models/ModModel.cs +++ b/src/Ryujinx.Ava/UI/Models/ModModel.cs @@ -27,4 +27,4 @@ public ModModel(string path, string name, bool enabled) Enabled = enabled; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index cdecae77d..d1401e53f 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -39,6 +39,7 @@ public class DownloadableContentManagerViewModel : BaseModel private string _search; private readonly ulong _titleId; + private IStorageProvider _storageProvider; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -90,8 +91,6 @@ public string UpdateCount get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); } - public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { _virtualFileSystem = virtualFileSystem; @@ -100,7 +99,7 @@ public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - StorageProvider = desktop.MainWindow.StorageProvider; + _storageProvider = desktop.MainWindow.StorageProvider; } _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); @@ -203,7 +202,7 @@ private Nca TryOpenNca(IStorage ncaStorage, string containerPath) public async void Add() { - var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle], AllowMultiple = true, diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 489d6d3a8..ed16a2aba 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -1,6 +1,7 @@ +using Avalonia; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; using Avalonia.Threading; using DynamicData; using Ryujinx.Ava.Common.Locale; @@ -10,7 +11,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS; -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -26,10 +26,9 @@ public class ModManagerViewModel : BaseModel public AvaloniaList _views = new(); public AvaloniaList _selectedMods = new(); - private ulong _titleId { get; } - private string _titleName { get; } - private string _search; + private ulong _titleId { get; } + private IStorageProvider _storageProvider; private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -81,13 +80,17 @@ public string ModCount get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); } - public ModManagerViewModel(ulong titleId, string titleName) + public ModManagerViewModel(ulong titleId) { _titleId = titleId; - _titleName = titleName; _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + _storageProvider = desktop.MainWindow.StorageProvider; + } + try { _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); @@ -230,19 +233,14 @@ private void AddMod(DirectoryInfo directory) public async void Add() { - OpenFolderDialog dialog = new() + var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] - }; + }); - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + foreach (var folder in result) { - string directory = await dialog.ShowAsync(desktop.MainWindow); - - if (directory != null) - { - AddMod(new DirectoryInfo(directory)); - } + AddMod(new DirectoryInfo(folder.Path.LocalPath)); } } @@ -268,4 +266,4 @@ public void DisableAll() SelectedMods.Clear(); } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index d56d43ea3..69372cb34 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -71,12 +71,11 @@ Padding="2.5"> + ItemsSource="{Binding Views}"> @@ -145,14 +144,14 @@ Name="AddButton" MinWidth="90" Margin="5" - Command="{ReflectionBinding Add}"> + Command="{Binding Add}"> @@ -177,4 +176,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index c7c2ded6f..70b1f4a55 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -24,21 +24,21 @@ public ModManagerWindow() InitializeComponent(); } - public ModManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public ModManagerWindow(ulong titleId) { - DataContext = ViewModel = new ModManagerViewModel(titleId, titleName); + DataContext = ViewModel = new ModManagerViewModel(titleId); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(ulong titleId, string titleName) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new ModManagerWindow(virtualFileSystem, titleId, titleName), + Content = new ModManagerWindow(titleId), Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], titleName, titleId.ToString("X16")) }; @@ -112,4 +112,4 @@ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/Mod.cs b/src/Ryujinx.Common/Configuration/Mod.cs index 970e59d26..a513a3994 100644 --- a/src/Ryujinx.Common/Configuration/Mod.cs +++ b/src/Ryujinx.Common/Configuration/Mod.cs @@ -5,4 +5,4 @@ public class Mod public string Name { get; set; } public string Path { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/ModMetadata.cs b/src/Ryujinx.Common/Configuration/ModMetadata.cs index 1b9366bb8..1999fdbca 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadata.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadata.cs @@ -6,4 +6,4 @@ public struct ModMetadata { public List Mods { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs index f1cd2cf21..8c1e242ad 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs @@ -7,4 +7,4 @@ namespace Ryujinx.Common.Configuration public partial class ModMetadataJsonSerializerContext : JsonSerializerContext { } -} \ No newline at end of file +} From 0aec0c501537c3c309109761d1056fda7f6afc9c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 13:01:47 +0100 Subject: [PATCH 12/25] Style fixes --- .../UI/ViewModels/ModManagerViewModel.cs | 20 +++++++++---------- src/Ryujinx.HLE/HOS/ModLoader.cs | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index ed16a2aba..4d4ac1878 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -19,18 +19,18 @@ namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { - public ModMetadata _modData; - public readonly string _modJsonPath; + private readonly ModMetadata _modData; + private readonly string _modJsonPath; - public AvaloniaList _mods = new(); - public AvaloniaList _views = new(); - public AvaloniaList _selectedMods = new(); + private AvaloniaList _mods = new(); + private AvaloniaList _views = new(); + private AvaloniaList _selectedMods = new(); private string _search; - private ulong _titleId { get; } - private IStorageProvider _storageProvider; + private ulong _titleId; + private readonly IStorageProvider _storageProvider; - private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public AvaloniaList Mods { @@ -93,7 +93,7 @@ public ModManagerViewModel(ulong titleId) try { - _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); + _modData = JsonHelper.DeserializeFromFile(_modJsonPath, _serializerContext.ModMetadata); } catch { @@ -186,7 +186,7 @@ public void Save() }); } - JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); + JsonHelper.SerializeToFile(_modJsonPath, _modData, _serializerContext.ModMetadata); } public void Delete(ModModel model) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index b67302470..169fc9413 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -38,7 +38,7 @@ public class ModLoader private const string AmsNroPatchDir = "nro_patches"; private const string AmsKipPatchDir = "kip_patches"; - private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public readonly struct Mod where T : FileSystemInfo { @@ -169,7 +169,7 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); } catch { @@ -275,7 +275,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); } catch { From 78eae90cb7456db9ae98d5b5780201e7e5474828 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 13:06:23 +0100 Subject: [PATCH 13/25] Final style fixes --- .../UI/ViewModels/DownloadableContentManagerViewModel.cs | 2 +- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index d1401e53f..a0d0c060c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -39,7 +39,7 @@ public class DownloadableContentManagerViewModel : BaseModel private string _search; private readonly ulong _titleId; - private IStorageProvider _storageProvider; + private readonly IStorageProvider _storageProvider; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 4d4ac1878..0a9767735 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -27,7 +27,7 @@ public class ModManagerViewModel : BaseModel private AvaloniaList _selectedMods = new(); private string _search; - private ulong _titleId; + private readonly ulong _titleId; private readonly IStorageProvider _storageProvider; private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); From 73439b9647099830416beb97eb6623cf61af2a7c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:24:34 +0100 Subject: [PATCH 14/25] Might be too general --- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 169fc9413..5ca91f416 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -183,14 +183,14 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin if (StrEquals(RomfsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); From 3f2c2a1465ef1a55327748005e6d325fc4e4bf13 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:39:54 +0100 Subject: [PATCH 15/25] Remove unnecessary using --- src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 70b1f4a55..72dcb1139 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -6,7 +6,6 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; using Button = Avalonia.Controls.Button; From 9979193bfd3e27fdad4a1f8794eaee199efa5475 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:48:58 +0100 Subject: [PATCH 16/25] Enable new mods by default --- .../UI/ViewModels/ModManagerViewModel.cs | 1 + src/Ryujinx.Common/Configuration/Mod.cs | 1 + src/Ryujinx.HLE/HOS/ModLoader.cs | 48 +++++++++++++++++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 0a9767735..f11a6a32a 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -183,6 +183,7 @@ public void Save() { Name = mod.Name, Path = mod.Path, + Enabled = mod.Enabled, }); } diff --git a/src/Ryujinx.Common/Configuration/Mod.cs b/src/Ryujinx.Common/Configuration/Mod.cs index a513a3994..052c7c8d1 100644 --- a/src/Ryujinx.Common/Configuration/Mod.cs +++ b/src/Ryujinx.Common/Configuration/Mod.cs @@ -4,5 +4,6 @@ public class Mod { public string Name { get; set; } public string Path { get; set; } + public bool Enabled { get; set; } } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 5ca91f416..eb6ea3e20 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -183,14 +183,34 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin if (StrEquals(RomfsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + { + enabled = modData.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + { + enabled = modData.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); @@ -285,7 +305,17 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + { + enabled = mod.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } @@ -293,7 +323,17 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + { + enabled = mod.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } From 928b91b3f6dc0bdb4771394c573c0e558d6fc8d6 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 15:00:11 +0100 Subject: [PATCH 17/25] More cleanup --- .../UI/ViewModels/ModManagerViewModel.cs | 26 ++---------- .../Configuration/ModMetadata.cs | 5 +++ src/Ryujinx.HLE/HOS/ModLoader.cs | 40 ++++++++++--------- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index f11a6a32a..f8cfba087 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -8,10 +8,8 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS; -using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,7 +17,6 @@ namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { - private readonly ModMetadata _modData; private readonly string _modJsonPath; private AvaloniaList _mods = new(); @@ -91,28 +88,13 @@ public ModManagerViewModel(ulong titleId) _storageProvider = desktop.MainWindow.StorageProvider; } - try - { - _modData = JsonHelper.DeserializeFromFile(_modJsonPath, _serializerContext.ModMetadata); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize mod data for {_titleId} at {_modJsonPath}"); - - _modData = new ModMetadata - { - Mods = new List() - }; - - Save(); - } - LoadMods(titleId); } private void LoadMods(ulong titleId) { Mods.Clear(); + SelectedMods.Clear(); string modsBasePath = ModLoader.GetModsBasePath(); @@ -175,11 +157,11 @@ private bool Filter(object arg) public void Save() { - _modData.Mods.Clear(); + ModMetadata modData = new(); foreach (ModModel mod in SelectedMods) { - _modData.Mods.Add(new Mod + modData.Mods.Add(new Mod { Name = mod.Name, Path = mod.Path, @@ -187,7 +169,7 @@ public void Save() }); } - JsonHelper.SerializeToFile(_modJsonPath, _modData, _serializerContext.ModMetadata); + JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata); } public void Delete(ModModel model) diff --git a/src/Ryujinx.Common/Configuration/ModMetadata.cs b/src/Ryujinx.Common/Configuration/ModMetadata.cs index 1999fdbca..174320d0a 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadata.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadata.cs @@ -5,5 +5,10 @@ namespace Ryujinx.Common.Configuration public struct ModMetadata { public List Mods { get; set; } + + public ModMetadata() + { + Mods = new List(); + } } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index eb6ea3e20..a74bb7ee1 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -164,16 +164,16 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { System.Text.StringBuilder types = new(); - string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); - ModMetadata? ModData = null; + string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); + ModMetadata modMetadata = new(); try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); + modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); } catch { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); } foreach (var modDir in dir.EnumerateDirectories()) @@ -185,11 +185,12 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { bool enabled; - if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + try { + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -202,11 +203,12 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { bool enabled; - if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + try { + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -290,16 +292,16 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t return; } - string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); - ModMetadata? ModData = null; + string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + ModMetadata modMetadata = new(); try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); + modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); } catch { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); } var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); @@ -307,11 +309,12 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t { bool enabled; - if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + try { - enabled = mod.Enabled; + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -325,11 +328,12 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t { bool enabled; - if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + try { - enabled = mod.Enabled; + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; From 243654efadaf9cc388bf6c2e0c28c231ef61b3a8 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 15:01:43 +0100 Subject: [PATCH 18/25] Fix saving metadata --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index f8cfba087..256222cb5 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -159,13 +159,13 @@ public void Save() { ModMetadata modData = new(); - foreach (ModModel mod in SelectedMods) + foreach (ModModel mod in Mods) { modData.Mods.Add(new Mod { Name = mod.Name, Path = mod.Path, - Enabled = mod.Enabled, + Enabled = SelectedMods.Contains(mod), }); } From 6dc8355edb712c98585cc45a1825c1df7ae84137 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 14:59:50 -0400 Subject: [PATCH 19/25] Dont deseralise ModMetadata several times --- src/Ryujinx.HLE/HOS/ModLoader.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index a74bb7ee1..6b6eae512 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -160,22 +160,10 @@ private static string EnsureBaseDirStructure(string modsBasePath) private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) => contentsDir.EnumerateDirectories(titleId, _dirEnumOptions).FirstOrDefault(); - private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId) + private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, ModMetadata modMetadata) { System.Text.StringBuilder types = new(); - string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); - ModMetadata modMetadata = new(); - - try - { - modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); - } - catch - { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); - } - foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); @@ -223,7 +211,7 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin } else { - AddModsFromDirectory(mods, modDir, titleId); + AddModsFromDirectory(mods, modDir, modMetadata); } if (types.Length > 0) @@ -342,7 +330,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } - AddModsFromDirectory(mods, titleDir, titleDir.Name); + AddModsFromDirectory(mods, titleDir, modMetadata); } public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) From 288ad6f9372bf8da08aec1ea49cc4f7464d609fa Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 15:25:58 -0400 Subject: [PATCH 20/25] Avalonia I hate you --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 256222cb5..ad3bcd95e 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -129,8 +129,6 @@ private void LoadMods(ulong titleId) Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } - SelectedMods = new(Mods.Where(x => x.Enabled)); - Sort(); } @@ -142,7 +140,12 @@ public void Sort() _views.Clear(); _views.AddRange(view); + + SelectedMods = new(Views.Where(x => x.Enabled)); + OnPropertyChanged(nameof(ModCount)); + OnPropertyChanged(nameof(Views)); + OnPropertyChanged(nameof(SelectedMods)); } private bool Filter(object arg) From 48d5bbff4d9ac5aab5453ed7d020b6603d74d9e4 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 15:45:26 -0400 Subject: [PATCH 21/25] Confirmation dialgoues --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 ++ .../UI/Windows/ModManagerWindow.axaml | 2 +- .../UI/Windows/ModManagerWindow.axaml.cs | 31 +++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 07c843142..340d488c6 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -397,6 +397,8 @@ "DialogUpdateAddUpdateErrorMessage": "The specified file does not contain an update for the selected title!", "DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading", "DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.", + "DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?", + "DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?", "SettingsTabGraphicsFeaturesOptions": "Features", "SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:", "CommonAuto": "Auto", diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index 69372cb34..d9f586408 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -151,7 +151,7 @@ Name="RemoveAllButton" MinWidth="90" Margin="5" - Command="{Binding DeleteAll}"> + Click="DeleteAll"> diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 72dcb1139..44fd363f2 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -46,7 +46,7 @@ public static async Task Show(ulong titleId, string titleName) contentDialog.Styles.Add(bottomBorder); - await ContentDialogHelper.ShowAsync(contentDialog); + await contentDialog.ShowAsync(); } private void SaveAndClose(object sender, RoutedEventArgs e) @@ -60,17 +60,42 @@ private void Close(object sender, RoutedEventArgs e) ((ContentDialog)Parent).Hide(); } - private void DeleteMod(object sender, RoutedEventArgs e) + private async void DeleteMod(object sender, RoutedEventArgs e) { if (sender is Button button) { if (button.DataContext is ModModel model) { - ViewModel.Delete(model); + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogModManagerDeletionWarningMessage, model.Name), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Delete(model); + } } } } + private async void DeleteAll(object sender, RoutedEventArgs e) + { + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance[LocaleKeys.DialogModManagerDeletionAllWarningMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.DeleteAll(); + } + } + private void OpenLocation(object sender, RoutedEventArgs e) { if (sender is Button button) From c910c1127e63dc6d079542460a2b77283a01f6ba Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Tue, 24 Oct 2023 19:03:24 -0400 Subject: [PATCH 22/25] Allow selecting multiple folders --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index ad3bcd95e..5a05766a9 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -221,7 +221,8 @@ public async void Add() { var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { - Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] + Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle], + AllowMultiple = true }); foreach (var folder in result) From fc8f81954bd9f019c0b3b1a9d6b3ce3c78118f1a Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:03:58 -0500 Subject: [PATCH 23/25] Add back secondary folder --- .../UI/ViewModels/ModManagerViewModel.cs | 4 +-- .../Configuration/AppDataManager.cs | 4 ++- src/Ryujinx.HLE/HOS/ModLoader.cs | 28 +++++++++++-------- .../Processes/Extensions/NcaExtensions.cs | 4 ++- .../Widgets/GameTableContextMenu.Designer.cs | 11 ++++++++ .../Ui/Widgets/GameTableContextMenu.cs | 9 +++++- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 5a05766a9..e1050211a 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -96,7 +96,7 @@ private void LoadMods(ulong titleId) Mods.Clear(); SelectedMods.Clear(); - string modsBasePath = ModLoader.GetModsBasePath(); + string modsBasePath = ModLoader.GetSdModsBasePath(); var modCache = new ModLoader.ModCache(); ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); @@ -187,7 +187,7 @@ public void Delete(ModModel model) private void AddMod(DirectoryInfo directory) { var directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories); - var destinationDir = ModLoader.GetTitleDir(ModLoader.GetModsBasePath(), _titleId.ToString("x16")); + var destinationDir = ModLoader.GetTitleDir(ModLoader.GetSdModsBasePath(), _titleId.ToString("x16")); foreach (var dir in directories) { diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 350a37a98..2b4a594d3 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -31,6 +31,7 @@ public enum LaunchMode public const string DefaultNandDir = "bis"; public const string DefaultSdcardDir = "sdcard"; + private const string DefaultModsDir = "mods"; public static string CustomModsPath { get; set; } public static string CustomSdModsPath { get; set; } @@ -150,6 +151,7 @@ private static void CopyDirectory(string sourceDir, string destinationDir) } } - public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; + public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName; + public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 6b6eae512..e9ec4d59e 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -143,6 +143,7 @@ private void Clear() private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); + public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); private static string EnsureBaseDirStructure(string modsBasePath) { @@ -443,7 +444,7 @@ private static IEnumerable GetCheatsInFile(FileInfo cheatFile) } // Assumes searchDirPaths don't overlap - private static void CollectMods(Dictionary modCaches, PatchCache patches) + private static void CollectMods(Dictionary modCaches, PatchCache patches, params string[] searchDirPaths) { static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || StrEquals(AmsNroPatchDir, name) || @@ -472,25 +473,28 @@ static bool TryQuery(DirectoryInfo searchDir, PatchCache patches, Dictionary titles) + public void CollectMods(IEnumerable titles, params string[] searchDirPaths) { Clear(); @@ -499,7 +503,7 @@ public void CollectMods(IEnumerable titles) _appMods[titleId] = new ModCache(); } - CollectMods(_appMods, _patches); + CollectMods(_appMods, _patches, searchDirPaths); } internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index de396ae9d..e369f4b04 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -35,7 +35,9 @@ public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca // Collecting mods related to AocTitleIds and ProgramId. device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId())); + device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), + ModLoader.GetModsBasePath(), + ModLoader.GetSdModsBasePath()); // Load Nacp file. var nacpData = new BlitStruct(1); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 370d22640..734437eea 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -11,6 +11,7 @@ public partial class GameTableContextMenu : Menu private MenuItem _manageDlcMenuItem; private MenuItem _manageCheatMenuItem; private MenuItem _openTitleModDirMenuItem; + private MenuItem _openTitleSdModDirMenuItem; private Menu _extractSubMenu; private MenuItem _extractMenuItem; private MenuItem _extractRomFsMenuItem; @@ -89,6 +90,15 @@ private void InitializeComponent() }; _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; + // + // _openTitleSdModDirMenuItem + // + _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory") + { + TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.", + }; + _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked; + // // _extractSubMenu // @@ -211,6 +221,7 @@ private void ShowComponent() Add(_manageDlcMenuItem); Add(_manageCheatMenuItem); Add(_openTitleModDirMenuItem); + Add(_openTitleSdModDirMenuItem); Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 425766b5a..82acb9ac4 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -496,7 +496,14 @@ private void ManageCheats_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args) { - string sdModsBasePath = ModLoader.GetModsBasePath(); + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); + + OpenHelper.OpenFolder(titleModsPath); + } + + private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { + string sdModsBasePath = ModLoader.GetSdModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); From a986fe3ed0ee37cf51c18ec9cc4f87a378a014f2 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:07:06 -0500 Subject: [PATCH 24/25] Search both paths --- .../UI/ViewModels/ModManagerViewModel.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index e1050211a..b125e7a37 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -96,37 +96,40 @@ private void LoadMods(ulong titleId) Mods.Clear(); SelectedMods.Clear(); - string modsBasePath = ModLoader.GetSdModsBasePath(); + string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()]; - var modCache = new ModLoader.ModCache(); - ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); - - foreach (var mod in modCache.RomfsDirs) + foreach (var path in modsBasePaths) { - var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); - if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + var modCache = new ModLoader.ModCache(); + ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), titleId); + + foreach (var mod in modCache.RomfsDirs) { - Mods.Add(modModel); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } - } - foreach (var mod in modCache.RomfsContainers) - { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); - } + foreach (var mod in modCache.RomfsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + } - foreach (var mod in modCache.ExefsDirs) - { - var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); - if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + foreach (var mod in modCache.ExefsDirs) { - Mods.Add(modModel); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } - } - foreach (var mod in modCache.ExefsContainers) - { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + foreach (var mod in modCache.ExefsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + } } Sort(); From e0acdc4d89a5346b5968995224d260a7e18c9ce2 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:13:21 -0500 Subject: [PATCH 25/25] Fix formatting --- src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 82acb9ac4..eb048b00d 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -502,7 +502,8 @@ private void OpenTitleModDir_Clicked(object sender, EventArgs args) OpenHelper.OpenFolder(titleModsPath); } - private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { + private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) + { string sdModsBasePath = ModLoader.GetSdModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);