From 038017393781dd3f8c9e65fe1b3e4811ddacb8a9 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Mon, 20 Jan 2025 09:28:48 -0600 Subject: [PATCH] UI: Dump DLC RomFS. You can access this in the Manage DLC screen, it's the new button on each DLC line. Closes #548 --- src/Ryujinx/Common/ApplicationHelper.cs | 137 ++++++++++++++++++ .../DownloadableContentManagerWindow.axaml | 10 ++ .../DownloadableContentManagerWindow.axaml.cs | 15 ++ 3 files changed, 162 insertions(+) diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index 14773114c..8fb2c11f3 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -308,6 +308,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( + $"Ryujinx {Program.Version} - {LocaleManager.Instance[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( + $"Ryujinx {Program.Version} - {LocaleManager.Instance[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 (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token) { diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index d53074499..47d9dd659 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -8,6 +8,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:models="clr-namespace:Ryujinx.UI.Common.Models;assembly=Ryujinx.UI.Common" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:pi="using:Projektanker.Icons.Avalonia" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" Width="500" Height="380" @@ -138,6 +139,15 @@ Spacing="10" Orientation="Horizontal" HorizontalAlignment="Right"> +