Просмотр исходного кода

UI: Dump DLC RomFS.
You can access this in the Manage DLC screen, it's the new button on each DLC line.

Closes #548

Evan Husted 1 год назад
Родитель
Сommit
b360f4e721

+ 137 - 0
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<IFile>();
+
+                        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<IFileSystem>(ncaFileSystem);
+                    using var uniqueOutputFs = new UniqueRef<IFileSystem>(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)

+ 16 - 1
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 @@
                 <ListBox.DataTemplates>
                     <DataTemplate
                         DataType="models:DownloadableContentModel">
-                        <Panel Margin="10">
+                        <Panel Margin="10" Background="Transparent">
+                            <Panel.ContextFlyout>
+                                <Flyout ShowMode="TransientWithDismissOnPointerMoveAway">
+
+                                </Flyout>
+                            </Panel.ContextFlyout>
                             <Grid ColumnDefinitions="*,Auto">
                                 <Grid
                                     Grid.Column="0" ColumnDefinitions="*,Auto">
@@ -119,6 +125,15 @@
                                     Spacing="10"
                                     Orientation="Horizontal"
                                     HorizontalAlignment="Right">
+                                    <Button 
+                                        VerticalAlignment="Center"
+                                        HorizontalAlignment="Right"
+                                        Padding="10"
+                                        MinWidth="0"
+                                        MinHeight="0"
+                                        Click="DlcItem_DumpRomfs">
+                                        <ui:SymbolIcon Symbol="MoveToFolder"/>
+                                    </Button>
                                     <Button
                                         VerticalAlignment="Center"
                                         HorizontalAlignment="Right"

+ 15 - 0
src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs

@@ -2,6 +2,8 @@ using Avalonia.Controls;
 using Avalonia.Interactivity;
 using Avalonia.Styling;
 using FluentAvalonia.UI.Controls;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Ava.Common;
 using Ryujinx.Ava.Common.Locale;
 using Ryujinx.Ava.Common.Models;
 using Ryujinx.Ava.UI.ViewModels;
@@ -93,5 +95,18 @@ namespace Ryujinx.Ava.UI.Windows
                 }
             }
         }
+        
+        private async void DlcItem_DumpRomfs(object sender, RoutedEventArgs e)
+        {
+            if (sender is not Button { DataContext: DownloadableContentModel dlc }) return;
+            if (RyujinxApp.MainWindow.ViewModel is not { } viewModel)
+                return;
+            
+            await ApplicationHelper.ExtractAoc(
+                viewModel.StorageProvider,
+                NcaSectionType.Data,
+                dlc.ContainerPath,
+                dlc.FileName);
+        }
     }
 }