diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Ava/Common | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Ava/Common')
| -rw-r--r-- | src/Ryujinx.Ava/Common/ApplicationHelper.cs | 411 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/Common/ApplicationSort.cs | 15 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs | 16 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs | 30 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/Common/Locale/LocaleManager.cs | 146 |
5 files changed, 618 insertions, 0 deletions
diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs new file mode 100644 index 00000000..8c36a636 --- /dev/null +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -0,0 +1,411 @@ +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +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.Helpers; +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.App.Common; +using Ryujinx.Ui.Common.Helper; +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.Common +{ + internal 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((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()) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.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; + } + + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.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; + } + + OpenSaveDir(saveDataId); + } + + public static void OpenSaveDir(ulong saveDataId) + { + 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 Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0) + { + OpenFolderDialog folderDialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] + }; + + string destination = await folderDialog.ShowAsync(_owner); + var cancellationToken = new CancellationTokenSource(); + + UpdateWaitWindow waitingDialog = new( + LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)), + cancellationToken); + + if (!string.IsNullOrWhiteSpace(destination)) + { + Thread extractorThread = new(() => + { + Dispatcher.UIThread.Post(waitingDialog.Show); + + 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") + { + pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).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(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]); + }); + + return; + } + + (Nca updatePatchNca, _) = ApplicationLibrary.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(async () => + { + waitingDialog.Close(); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]); + }); + } + else if (resultCode.Value.IsSuccess()) + { + Dispatcher.UIThread.Post(waitingDialog.Close); + + NotificationHelper.Show( + LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", + NotificationType.Information); + } + } + + 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); + }); + } + }); + + 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 diff --git a/src/Ryujinx.Ava/Common/ApplicationSort.cs b/src/Ryujinx.Ava/Common/ApplicationSort.cs new file mode 100644 index 00000000..6ff06a1e --- /dev/null +++ b/src/Ryujinx.Ava/Common/ApplicationSort.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.Ava.Common +{ + internal enum ApplicationSort + { + Title, + TitleId, + Developer, + LastPlayed, + TotalTimePlayed, + FileType, + FileSize, + Path, + Favorite + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs b/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs new file mode 100644 index 00000000..e85bdf34 --- /dev/null +++ b/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Ava.Common +{ + public enum KeyboardHotkeyState + { + None, + ToggleVSync, + Screenshot, + ShowUi, + Pause, + ToggleMute, + ResScaleUp, + ResScaleDown, + VolumeUp, + VolumeDown + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs b/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs new file mode 100644 index 00000000..b82c405d --- /dev/null +++ b/src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs @@ -0,0 +1,30 @@ +using Avalonia.Data; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.MarkupExtensions; +using System; + +namespace Ryujinx.Ava.Common.Locale +{ + internal class LocaleExtension : MarkupExtension + { + public LocaleExtension(LocaleKeys key) + { + Key = key; + } + + public LocaleKeys Key { get; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + LocaleKeys keyToUse = Key; + + ReflectionBindingExtension binding = new($"[{keyToUse}]") + { + Mode = BindingMode.OneWay, + Source = LocaleManager.Instance + }; + + return binding.ProvideValue(serviceProvider); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/Common/Locale/LocaleManager.cs b/src/Ryujinx.Ava/Common/Locale/LocaleManager.cs new file mode 100644 index 00000000..464ab780 --- /dev/null +++ b/src/Ryujinx.Ava/Common/Locale/LocaleManager.cs @@ -0,0 +1,146 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common; +using Ryujinx.Common.Utilities; +using Ryujinx.Ui.Common.Configuration; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; + +namespace Ryujinx.Ava.Common.Locale +{ + class LocaleManager : BaseModel + { + private const string DefaultLanguageCode = "en_US"; + + private Dictionary<LocaleKeys, string> _localeStrings; + private Dictionary<LocaleKeys, string> _localeDefaultStrings; + private readonly ConcurrentDictionary<LocaleKeys, object[]> _dynamicValues; + + public static LocaleManager Instance { get; } = new LocaleManager(); + + public LocaleManager() + { + _localeStrings = new Dictionary<LocaleKeys, string>(); + _localeDefaultStrings = new Dictionary<LocaleKeys, string>(); + _dynamicValues = new ConcurrentDictionary<LocaleKeys, object[]>(); + + Load(); + } + + public void Load() + { + // Load the system Language Code. + string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_'); + + // If the view is loaded with the UI Previewer detached, then override it with the saved one or default. + if (Program.PreviewerDetached) + { + if (!string.IsNullOrEmpty(ConfigurationState.Instance.Ui.LanguageCode.Value)) + { + localeLanguageCode = ConfigurationState.Instance.Ui.LanguageCode.Value; + } + else + { + localeLanguageCode = DefaultLanguageCode; + } + } + + // Load en_US as default, if the target language translation is incomplete. + LoadDefaultLanguage(); + + LoadLanguage(localeLanguageCode); + } + + public string this[LocaleKeys key] + { + get + { + // Check if the locale contains the key. + if (_localeStrings.TryGetValue(key, out string value)) + { + // Check if the localized string needs to be formatted. + if (_dynamicValues.TryGetValue(key, out var dynamicValue)) + { + try + { + return string.Format(value, dynamicValue); + } + catch (Exception) + { + // If formatting failed use the default text instead. + if (_localeDefaultStrings.TryGetValue(key, out value)) + { + try + { + return string.Format(value, dynamicValue); + } + catch (Exception) + { + // If formatting the default text failed return the key. + return key.ToString(); + } + } + } + } + + return value; + } + + // If the locale doesn't contain the key return the default one. + if (_localeDefaultStrings.TryGetValue(key, out string defaultValue)) + { + return defaultValue; + } + + // If the locale text doesn't exist return the key. + return key.ToString(); + } + set + { + _localeStrings[key] = value; + + OnPropertyChanged(); + } + } + + public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values) + { + _dynamicValues[key] = values; + + OnPropertyChanged("Item"); + + return this[key]; + } + + private void LoadDefaultLanguage() + { + _localeDefaultStrings = LoadJsonLanguage(); + } + + public void LoadLanguage(string languageCode) + { + foreach (var item in LoadJsonLanguage(languageCode)) + { + this[item.Key] = item.Value; + } + } + + private Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode = DefaultLanguageCode) + { + var localeStrings = new Dictionary<LocaleKeys, string>(); + string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json"); + var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary); + + foreach (var item in strings) + { + if (Enum.TryParse<LocaleKeys>(item.Key, out var key)) + { + localeStrings[key] = item.Value; + } + } + + return localeStrings; + } + } +}
\ No newline at end of file |
