aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Common/ApplicationHelper.cs
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-05-15 11:30:15 +0000
committerGitHub <noreply@github.com>2022-05-15 13:30:15 +0200
commitdeb99d2cae3e80bdf70cb52c6c160094dc7c9292 (patch)
treee60f44d1b4bd45bbf36fcfa750fb99787febfdbe /Ryujinx.Ava/Common/ApplicationHelper.cs
parent9ba73ffbe5f78c0403cf102b95768f388da05122 (diff)
Avalonia UI - Part 1 (#3270)1.1.122
* avalonia part 1 * remove vulkan ui backend * move ui common files to ui common project * get name for oading screen from device * rebase. * review 1 * review 1.1 * review * cleanup * addressed review * use cancellation token * review * review * rebased * cancel library loading when closing window * remove star image, use fonticon instead * delete render control frame buffer when game ends. change position of fav star * addressed @Thog review * ensure the right ui is downloaded in updates * fix crash when showing not supported dialog during controller request * add prefix to artifact names * Auto-format Avalonia project * Fix input * Fix build, simplify app disposal * remove nv stutter thread * addressed review * add missing change * maintain window size if new size is zero length * add game, handheld, docked to local * reverse scale main window * Update de_DE.json * Update de_DE.json * Update de_DE.json * Update italian json * Update it_IT.json * let render timer poll with no wait * remove unused code * more unused code * enabled tiered compilation and trimming * check if window event is not closed before signaling * fix atmospher case * locale fix * locale fix * remove explicit tiered compilation declarations * Remove ) it_IT.json * Remove ) de_DE.json * Update it_IT.json * Update pt_BR locale with latest strings * Remove ')' * add more strings to locale * update locale * remove extra slash * remove extra slash * set firmware version to 0 if key's not found * fix * revert timer changes * lock on object instead * Update it_IT.json * remove unused method * add load screen text to locale * drop swap event * Update de_DE.json * Update de_DE.json * do null check when stopping emulator * Update de_DE.json * Create tr_TR.json * Add tr_TR * Add tr_TR + Turkish * Update it_IT.json * Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * addressed review * Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs Co-authored-by: gdkchan <gab.dark.100@gmail.com> * use avalonia's inbuilt renderer on linux * removed whitespace * workaround for queue render crash with vsync off * drop custom backend * format files * fix not closing issue * remove warnings * rebase * update avalonia library * Reposition the Text and Button on About Page * Assign build version * Remove appveyor text Co-authored-by: gdk <gab.dark.100@gmail.com> Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com> Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com> Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com> Co-authored-by: Ac_K <Acoustik666@gmail.com> Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/Common/ApplicationHelper.cs')
-rw-r--r--Ryujinx.Ava/Common/ApplicationHelper.cs417
1 files changed, 417 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Common/ApplicationHelper.cs b/Ryujinx.Ava/Common/ApplicationHelper.cs
new file mode 100644
index 00000000..877eaf71
--- /dev/null
+++ b/Ryujinx.Ava/Common/ApplicationHelper.cs
@@ -0,0 +1,417 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using LibHac;
+using LibHac.Account;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.Fs.Shim;
+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.Windows;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Ui.Common.Helper;
+using System;
+using System.Buffers;
+using System.IO;
+using System.Threading;
+using static LibHac.Fs.ApplicationSaveDataManagement;
+using Path = System.IO.Path;
+
+namespace Ryujinx.Ava.Common
+{
+ public static class ApplicationHelper
+ {
+ private static HorizonClient _horizonClient;
+ private static AccountManager _accountManager;
+ private static VirtualFileSystem _virtualFileSystem;
+ private static StyleableWindow _owner;
+
+ public static void Initialize(VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, StyleableWindow owner)
+ {
+ _owner = owner;
+ _virtualFileSystem = virtualFileSystem;
+ _horizonClient = horizonClient;
+ _accountManager = accountManager;
+ }
+
+ private static bool TryFindSaveData(string titleName, ulong titleId,
+ BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
+ {
+ saveDataId = default;
+
+ Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo,
+ SaveDataSpaceId.User, in filter);
+
+ if (ResultFs.TargetNotFound.Includes(result))
+ {
+ ref ApplicationControlProperty control = ref controlHolder.Value;
+
+ Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {titleName} [{titleId:x16}]");
+
+ if (Utilities.IsZeros(controlHolder.ByteSpan))
+ {
+ // If the current application doesn't have a loaded control property, create a dummy one
+ // and set the savedata sizes so a user savedata will be created.
+ control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
+
+ // The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
+ control.UserAccountSaveDataSize = 0x4000;
+ control.UserAccountSaveDataJournalSize = 0x4000;
+
+ Logger.Warning?.Print(LogClass.Application,
+ "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
+ }
+
+ Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
+
+ result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
+
+ if (result.IsFailure())
+ {
+ ContentDialogHelper.CreateErrorDialog(_owner,
+ string.Format(LocaleManager.Instance["DialogMessageCreateSaveErrorMessage"], result.ToStringWithName()));
+
+ return false;
+ }
+
+ // Try to find the savedata again after creating it
+ result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter);
+ }
+
+ if (result.IsSuccess())
+ {
+ saveDataId = saveDataInfo.SaveDataId;
+
+ return true;
+ }
+
+ ContentDialogHelper.CreateErrorDialog(_owner,
+ string.Format(LocaleManager.Instance["DialogMessageFindSaveErrorMessage"], result.ToStringWithName()));
+
+ return false;
+ }
+
+ public static void OpenSaveDir(in SaveDataFilter saveDataFilter, ulong titleId,
+ BlitStruct<ApplicationControlProperty> controlData, string titleName)
+ {
+ if (!TryFindSaveData(titleName, titleId, controlData, in saveDataFilter, out ulong saveDataId))
+ {
+ return;
+ }
+
+ string saveRootPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}");
+
+ if (!Directory.Exists(saveRootPath))
+ {
+ // Inconsistent state. Create the directory
+ Directory.CreateDirectory(saveRootPath);
+ }
+
+ string committedPath = Path.Combine(saveRootPath, "0");
+ string workingPath = Path.Combine(saveRootPath, "1");
+
+ // If the committed directory exists, that path will be loaded the next time the savedata is mounted
+ if (Directory.Exists(committedPath))
+ {
+ OpenHelper.OpenFolder(committedPath);
+ }
+ else
+ {
+ // If the working directory exists and the committed directory doesn't,
+ // the working directory will be loaded the next time the savedata is mounted
+ if (!Directory.Exists(workingPath))
+ {
+ Directory.CreateDirectory(workingPath);
+ }
+
+ OpenHelper.OpenFolder(workingPath);
+ }
+ }
+
+ public static async void ExtractSection(NcaSectionType ncaSectionType, string titleFilePath,
+ int programIndex = 0)
+ {
+ OpenFolderDialog folderDialog = new() { Title = LocaleManager.Instance["FolderDialogExtractTitle"] };
+
+ string destination = await folderDialog.ShowAsync(_owner);
+
+ var cancellationToken = new CancellationTokenSource();
+
+ if (!string.IsNullOrWhiteSpace(destination))
+ {
+ Thread extractorThread = new(() =>
+ {
+ Dispatcher.UIThread.Post(async () =>
+ {
+ UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
+ _owner,
+ string.Format(LocaleManager.Instance["DialogNcaExtractionMessage"], ncaSectionType, Path.GetFileName(titleFilePath)),
+ "",
+ "",
+ LocaleManager.Instance["InputDialogCancel"],
+ LocaleManager.Instance["DialogNcaExtractionTitle"]);
+
+ if (result == UserResult.Cancel)
+ {
+ cancellationToken.Cancel();
+ }
+ });
+
+ Thread.Sleep(1000);
+
+ using (FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read))
+ {
+ Nca mainNca = null;
+ Nca patchNca = null;
+
+ string extension = Path.GetExtension(titleFilePath).ToLower();
+
+ if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
+ {
+ PartitionFileSystem pfs;
+
+ if (extension == ".xci")
+ {
+ Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
+
+ pfs = xci.OpenPartition(XciPartitionType.Secure);
+ }
+ else
+ {
+ pfs = new PartitionFileSystem(file.AsStorage());
+ }
+
+ 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 == NcaContentType.Program)
+ {
+ int dataIndex =
+ Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+ if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
+ {
+ patchNca = nca;
+ }
+ else
+ {
+ mainNca = nca;
+ }
+ }
+ }
+ }
+ else if (extension == ".nca")
+ {
+ mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
+ }
+
+ if (mainNca == null)
+ {
+ Logger.Error?.Print(LogClass.Application,
+ "Extraction failure. The main NCA was not present in the selected file");
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionMainNcaNotFoundErrorMessage"]);
+ });
+ return;
+ }
+
+ (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem,
+ mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+ if (updatePatchNca != null)
+ {
+ patchNca = updatePatchNca;
+ }
+
+ int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
+
+ try
+ {
+ IFileSystem ncaFileSystem = patchNca != null
+ ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
+ : mainNca.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(() =>
+ {
+ ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionCheckLogErrorMessage"]);
+ });
+ }
+ else if (resultCode.Value.IsSuccess())
+ {
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await ContentDialogHelper.CreateInfoDialog(
+ _owner,
+ LocaleManager.Instance["DialogNcaExtractionSuccessMessage"],
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ "",
+ LocaleManager.Instance["DialogNcaExtractionTitle"]);
+ });
+ }
+ }
+
+ fsClient.Unmount(source.ToU8Span());
+ fsClient.Unmount(output.ToU8Span());
+ }
+ catch (ArgumentException ex)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
+ });
+ }
+ }
+ });
+
+ extractorThread.Name = "GUI.NcaSectionExtractorThread";
+ extractorThread.IsBackground = true;
+ extractorThread.Start();
+ }
+ }
+
+ public static (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token)
+ {
+ Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All);
+ if (rc.IsFailure())
+ {
+ return (rc, false);
+ }
+
+ using (sourceHandle)
+ {
+ foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default))
+ {
+ if (token.IsCancellationRequested)
+ {
+ return (null, true);
+ }
+
+ string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name));
+ string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name));
+
+ if (entry.Type == DirectoryEntryType.Directory)
+ {
+ fs.EnsureDirectoryExists(subDstPath);
+
+ (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath, token);
+ if (canceled || result.Value.IsFailure())
+ {
+ return (result, canceled);
+ }
+ }
+
+ if (entry.Type == DirectoryEntryType.File)
+ {
+ fs.CreateOrOverwriteFile(subDstPath, entry.Size);
+
+ rc = CopyFile(fs, subSrcPath, subDstPath);
+ if (rc.IsFailure())
+ {
+ return (rc, false);
+ }
+ }
+ }
+ }
+
+ return (Result.Success, false);
+ }
+
+ public static Result CopyFile(FileSystemClient fs, string sourcePath, string destPath)
+ {
+ Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ using (sourceHandle)
+ {
+ rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ using (destHandle)
+ {
+ const int MaxBufferSize = 1024 * 1024;
+
+ rc = fs.GetFileSize(out long fileSize, sourceHandle);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ int bufferSize = (int)Math.Min(MaxBufferSize, fileSize);
+
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
+ try
+ {
+ for (long offset = 0; offset < fileSize; offset += bufferSize)
+ {
+ int toRead = (int)Math.Min(fileSize - offset, bufferSize);
+ Span<byte> buf = buffer.AsSpan(0, toRead);
+
+ rc = fs.ReadFile(out long _, sourceHandle, offset, buf);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+
+ rc = fs.FlushFile(destHandle);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+ }
+ }
+
+ return Result.Success;
+ }
+ }
+} \ No newline at end of file