diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index 7db933ed6..88a991a3d 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -295,6 +295,143 @@ namespace Ryujinx.Ava.Common }; extractorThread.Start(); } + + public static void ExtractAoc(string destination, NcaSectionType ncaSectionType, string updateFilePath, string updateName) + { + var cancellationToken = new CancellationTokenSource(); + + UpdateWaitWindow waitingDialog = new( + RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(updateFilePath)), + cancellationToken); + + Thread extractorThread = new(() => + { + Dispatcher.UIThread.Post(waitingDialog.Show); + + using FileStream file = new(updateFilePath, FileMode.Open, FileAccess.Read); + + Nca publicDataNca = null; + + string extension = Path.GetExtension(updateFilePath).ToLower(); + if (extension is ".nsp") + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + IFileSystem pfs = pfsTemp; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + if (nca.Header.ContentType is NcaContentType.PublicData && nca.SectionExists(NcaSectionType.Data)) + { + publicDataNca = nca; + } + } + } + + if (publicDataNca is null) + { + Logger.Error?.Print(LogClass.Application, "Extraction failure. The NCA was not present in the selected file"); + + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); + }); + + return; + } + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + int index = Nca.GetSectionIndexFromType(ncaSectionType, publicDataNca.Header.ContentType); + + try + { + IFileSystem ncaFileSystem = publicDataNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); + + FileSystemClient fsClient = _horizonClient.Fs; + + string source = DateTime.Now.ToFileTime().ToString()[10..]; + string output = DateTime.Now.ToFileTime().ToString()[10..]; + + using var uniqueSourceFs = new UniqueRef(ncaFileSystem); + using var uniqueOutputFs = new UniqueRef(new LocalFileSystem(destination)); + + fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref); + fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref); + + (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token); + + if (!canceled) + { + if (resultCode.Value.IsFailure()) + { + Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); + + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); + }); + } + else if (resultCode.Value.IsSuccess()) + { + Dispatcher.UIThread.Post(waitingDialog.Close); + + NotificationHelper.ShowInformation( + RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), + $"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}"); + } + } + + fsClient.Unmount(source.ToU8Span()); + fsClient.Unmount(output.ToU8Span()); + } + catch (ArgumentException ex) + { + Logger.Error?.Print(LogClass.Application, $"{ex.Message}"); + + Dispatcher.UIThread.InvokeAsync(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(ex.Message); + }); + } + }) + { + Name = "GUI.NcaSectionExtractorThread", + IsBackground = true, + }; + extractorThread.Start(); + } + + public static async Task ExtractAoc(IStorageProvider storageProvider, NcaSectionType ncaSectionType, + string updateFilePath, string updateName) + { + var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], + AllowMultiple = false, + }); + + if (result.Count == 0) + { + return; + } + + ExtractAoc(result[0].Path.LocalPath, ncaSectionType, updateFilePath, updateName); + } public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0) diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index 8efcfcadc..ab057e856 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -7,6 +7,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:pi="using:Projektanker.Icons.Avalonia" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:models="clr-namespace:Ryujinx.Ava.Common.Models" Width="500" @@ -89,7 +90,12 @@ - + + + + + + @@ -119,6 +125,15 @@ Spacing="10" Orientation="Horizontal" HorizontalAlignment="Right"> +