diff --git a/Ryujinx.Ava/Common/AutoDownloadableContentLoader.cs b/Ryujinx.Ava/Common/AutoDownloadableContentLoader.cs new file mode 100644 index 000000000..e893ca560 --- /dev/null +++ b/Ryujinx.Ava/Common/AutoDownloadableContentLoader.cs @@ -0,0 +1,188 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Drawing.Printing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class AutoDownloadableContentLoader + { + public readonly List Applications; + public AvaloniaList DownloadableContents { get; private set; } + private readonly VirtualFileSystem FileSystem; + + public AutoDownloadableContentLoader(Collection applications, VirtualFileSystem virtualFileSystem) + { + Applications = applications.ToList(); + FileSystem = virtualFileSystem; + } + + public async Task AutoLoadDlcsAsync(ApplicationData application, Dictionary dlcPathAndGameNames) + { + DownloadableContents = new AvaloniaList(); + + char[] bannedSymbols = { '.', ',', ':', ';', '>', '<', '\'', '\"', }; + string gameTitle = string.Join("", application.TitleName.Split(bannedSymbols)).ToLower().Trim(); + + + //Loops through the Dlcs to the given gameTitle and adds them to the downloadableContent List + dlcPathAndGameNames.Where(titleDlc => titleDlc.Value.ToLower() == gameTitle) + .ToList() + .ForEach(async dlc => await AddDownloadableContent(dlc.Key, application)); + + + List downloadableContentContainers = new List(); + string jsonPath = LoadJsonFromTitle(application, downloadableContentContainers); + Save(jsonPath, downloadableContentContainers); + } + + + private async Task AddDownloadableContent(string path, ApplicationData applicationData) + { + if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) + { + return; + } + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + bool containsDownloadableContent = false; + + FileSystem.ImportTickets(partitionFileSystem); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != ulong.Parse(applicationData.TitleId, NumberStyles.HexNumber)) + { + break; + } + + DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true)); + + containsDownloadableContent = true; + } + } + + if (!containsDownloadableContent) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]); + } + } + + + private Nca TryOpenNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(FileSystem.KeySet, ncaStorage); + } + catch (Exception ex) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath)); + }); + } + + return null; + } + + + private string LoadJsonFromTitle(ApplicationData applicationdata, List _downloadableContentContainerList) + { + ulong titleId = ulong.Parse(applicationdata.TitleId, NumberStyles.HexNumber); + + string _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); + + try + { + _downloadableContentContainerList = JsonHelper.DeserializeFromFile>(_downloadableContentJsonPath); + } + catch + { + _downloadableContentContainerList = new List(); + } + + return _downloadableContentJsonPath; + } + + + public void Save(string _downloadableContentJsonPath, List _downloadableContentContainerList) + { + _downloadableContentContainerList.Clear(); + + DownloadableContentContainer container = default; + + foreach (DownloadableContentModel downloadableContent in DownloadableContents) + { + if (container.ContainerPath != downloadableContent.ContainerPath) + { + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + _downloadableContentContainerList.Add(container); + } + + container = new DownloadableContentContainer + { + ContainerPath = downloadableContent.ContainerPath, + DownloadableContentNcaList = new List() + }; + } + + container.DownloadableContentNcaList.Add(new DownloadableContentNca + { + Enabled = downloadableContent.Enabled, + TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16), + FullPath = downloadableContent.FullPath + }); + } + + if (!string.IsNullOrWhiteSpace(container.ContainerPath)) + { + _downloadableContentContainerList.Add(container); + } + + using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough)) + { + downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true))); + } + } + } +} diff --git a/Ryujinx.Ava/Common/AutoTilteUpdateLoader.cs b/Ryujinx.Ava/Common/AutoTilteUpdateLoader.cs new file mode 100644 index 000000000..6b0efd5d3 --- /dev/null +++ b/Ryujinx.Ava/Common/AutoTilteUpdateLoader.cs @@ -0,0 +1,156 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Path = System.IO.Path; +using SpanHelpers = LibHac.Common.SpanHelpers; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class AutoTitleUpdateLoader + { + private TitleUpdateMetadata _titleUpdateWindowData; + public List Applications { get; private set; } + private readonly VirtualFileSystem FileSystem; + private AvaloniaList titleUpdates { get; set; } + + public AutoTitleUpdateLoader(Collection applications, VirtualFileSystem virtualFileSystem) + { + Applications = applications.ToList(); + FileSystem = virtualFileSystem; + } + + + public async Task AutoLoadUpdatesAsync(ApplicationData application, Dictionary updatePathandGameNames) + { + titleUpdates = new AvaloniaList(); + + char[] bannedSymbols = { '.', ',', ':', ';', '>', '<', '\'', '\"', }; + string gameTitle = string.Join("", application.TitleName.Split(bannedSymbols)).ToLower().Trim(); + + + //Loops through the Updates to the given gameTitle and adds them to the downloadableContent List + updatePathandGameNames.Where(titleUpdate => titleUpdate.Value.ToLower() == gameTitle) + .ToList() + .ForEach(async update => await AddUpdate(update.Key, application)); + + + List downloadableContentContainers = new List(); + string jsonPath = LoadJsonFromTitle(application, downloadableContentContainers); + Save(jsonPath); + } + + private string LoadJsonFromTitle(ApplicationData applicationdata, List _downloadableContentContainerList) + { + ulong titleId = ulong.Parse(applicationdata.TitleId, NumberStyles.HexNumber); + + string _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_titleUpdateJsonPath); + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List() + }; + } + + return _titleUpdateJsonPath; + } + + private async Task AddUpdate(string path, ApplicationData applicationData) + { + if (File.Exists(path) && !titleUpdates.Any(x => x.Path == path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(FileSystem, new PartitionFileSystem(file.AsStorage()), ulong.Parse(applicationData.TitleId, NumberStyles.HexNumber).ToString("x16"), 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using UniqueRef nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + titleUpdates.Add(new TitleUpdateModel(controlData, path)); + + foreach (var update in titleUpdates) + { + update.IsEnabled = false; + } + + titleUpdates.Last().IsEnabled = true; + } + else + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]); + }); + } + } + catch (Exception ex) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path)); + }); + } + } + } + + public void Save(string titleUpdateJsonPath) + { + _titleUpdateWindowData.Paths.Clear(); + + _titleUpdateWindowData.Selected = ""; + + foreach (TitleUpdateModel update in titleUpdates) + { + _titleUpdateWindowData.Paths.Add(update.Path); + + if (update.IsEnabled) + { + _titleUpdateWindowData.Selected = update.Path; + } + } + + using (FileStream titleUpdateJsonStream = File.Create(titleUpdateJsonPath, 4096, FileOptions.WriteThrough)) + { + titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs index 06513e37c..e90301784 100644 --- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs @@ -30,6 +30,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Path = System.IO.Path; @@ -943,6 +944,58 @@ namespace Ryujinx.Ava.Ui.ViewModels OpenHelper.OpenFolder(logPath); } + public static void OpenGlobalDlcsFolder() + { + string globalDlcsPath = Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalDlcs"); + + new DirectoryInfo(globalDlcsPath).Create(); + + OpenHelper.OpenFolder(globalDlcsPath); + } + + public static void OpenGlobalUpdatesFolder() + { + string golbalUpdatesPath = Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalUpdates"); + + new DirectoryInfo(golbalUpdatesPath).Create(); + + OpenHelper.OpenFolder(golbalUpdatesPath); + } + + public async void LoadGlobalDlcs() + { + AutoDownloadableContentLoader downloadableContentManager = new AutoDownloadableContentLoader(Applications, _owner.VirtualFileSystem); + + //Searches for the files in the Global Dlcs folder and puts their path and titlename (from folder) in a dictionary. + Dictionary dlcsAndUpdatesPathWithGameName = Directory.GetFiles(Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalDlcs")) + .ToDictionary(x => x, y => Path.GetFileName(y).Split(new[] { "[" }, StringSplitOptions.RemoveEmptyEntries) + .First() + .Trim()); + + + foreach (ApplicationData application in Applications) + { + await downloadableContentManager.AutoLoadDlcsAsync(application, dlcsAndUpdatesPathWithGameName); + } + } + + public async void LoadGlobalUpdates() + { + AutoTitleUpdateLoader titleUpdateManager = new AutoTitleUpdateLoader(Applications, _owner.VirtualFileSystem); + + //Searches for the files in the Global Updates folder and puts their path and titlename (from folder) in a dictionary. + Dictionary dlcsAndUpdatesPathWithGameName = Directory.GetFiles(Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalUpdates")) + .ToDictionary(x => x, y => Path.GetFileName(y).Split(new[] { "[" }, StringSplitOptions.RemoveEmptyEntries) + .First() + .Trim()); + + + foreach (ApplicationData application in Applications) + { + await titleUpdateManager.AutoLoadUpdatesAsync(application, dlcsAndUpdatesPathWithGameName); + } + } + public void ToggleFullscreen() { if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs) diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml index 387587b62..a39876c57 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml @@ -103,7 +103,25 @@ Command="{ReflectionBinding OpenLogsFolder}" Header="{locale:Locale MenuBarFileOpenLogsFolder}" ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" /> - + + + + + + +