From f06d22d6f01e657ebbc0c8ef082739cd468e47b5 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sun, 11 Feb 2024 02:09:18 +0000 Subject: Infra: Capitalisation Consistency (#6296) * Rename Ryujinx.UI.Common * Rename Ryujinx.UI.LocaleGenerator * Update in Files AboutWindow * Configuration State * Rename projects * Ryujinx/UI * Fix build * Main remaining inconsistencies * HLE.UI Namespace * HLE.UI Files * Namespace * Ryujinx.UI.Common.Configuration.UI * Ryujinx.UI.Common,Configuration.UI Files * More instances --- .../App/ApplicationAddedEventArgs.cs | 9 + .../App/ApplicationCountUpdatedEventArgs.cs | 10 + src/Ryujinx.UI.Common/App/ApplicationData.cs | 158 ++ .../App/ApplicationJsonSerializerContext.cs | 10 + src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 930 ++++++++++++ src/Ryujinx.UI.Common/App/ApplicationMetadata.cs | 51 + .../Configuration/AudioBackend.cs | 14 + .../Configuration/ConfigurationFileFormat.cs | 409 +++++ .../ConfigurationFileFormatSettings.cs | 9 + .../ConfigurationJsonSerializerContext.cs | 10 + .../Configuration/ConfigurationState.cs | 1562 ++++++++++++++++++++ src/Ryujinx.UI.Common/Configuration/FileTypes.cs | 12 + .../Configuration/LoggerModule.cs | 113 ++ .../Configuration/System/Language.cs | 28 + .../Configuration/System/Region.cs | 17 + .../Configuration/UI/ColumnSort.cs | 8 + .../Configuration/UI/GuiColumns.cs | 16 + .../Configuration/UI/ShownFileTypes.cs | 12 + .../Configuration/UI/WindowStartup.cs | 11 + src/Ryujinx.UI.Common/DiscordIntegrationModule.cs | 98 ++ .../Extensions/FileTypeExtensions.cs | 25 + src/Ryujinx.UI.Common/Helper/CommandLineState.cs | 99 ++ src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs | 50 + .../Helper/FileAssociationHelper.cs | 202 +++ src/Ryujinx.UI.Common/Helper/LinuxHelper.cs | 62 + src/Ryujinx.UI.Common/Helper/ObjectiveC.cs | 160 ++ src/Ryujinx.UI.Common/Helper/OpenHelper.cs | 112 ++ src/Ryujinx.UI.Common/Helper/SetupValidator.cs | 114 ++ src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs | 162 ++ src/Ryujinx.UI.Common/Helper/TitleHelper.cs | 30 + src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs | 219 +++ src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs | 67 + .../Models/Amiibo/AmiiboApiGamesSwitch.cs | 15 + .../Models/Amiibo/AmiiboApiUsage.cs | 12 + src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs | 14 + .../Models/Amiibo/AmiiboJsonSerializerContext.cs | 9 + .../Github/GithubReleaseAssetJsonResponse.cs | 9 + .../Models/Github/GithubReleasesJsonResponse.cs | 10 + .../Github/GithubReleasesJsonSerializerContext.cs | 9 + .../Resources/Controller_JoyConLeft.svg | 1 + .../Resources/Controller_JoyConPair.svg | 1 + .../Resources/Controller_JoyConRight.svg | 1 + .../Resources/Controller_ProCon.svg | 132 ++ src/Ryujinx.UI.Common/Resources/Icon_NCA.png | Bin 0 -> 10190 bytes src/Ryujinx.UI.Common/Resources/Icon_NRO.png | Bin 0 -> 10254 bytes src/Ryujinx.UI.Common/Resources/Icon_NSO.png | Bin 0 -> 10354 bytes src/Ryujinx.UI.Common/Resources/Icon_NSP.png | Bin 0 -> 9899 bytes src/Ryujinx.UI.Common/Resources/Icon_XCI.png | Bin 0 -> 9809 bytes src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png | Bin 0 -> 10573 bytes .../Resources/Logo_Discord_Dark.png | Bin 0 -> 9835 bytes .../Resources/Logo_Discord_Light.png | Bin 0 -> 10765 bytes .../Resources/Logo_GitHub_Dark.png | Bin 0 -> 4837 bytes .../Resources/Logo_GitHub_Light.png | Bin 0 -> 5166 bytes .../Resources/Logo_Patreon_Dark.png | Bin 0 -> 52210 bytes .../Resources/Logo_Patreon_Light.png | Bin 0 -> 29395 bytes src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png | Bin 0 -> 52972 bytes .../Resources/Logo_Twitter_Dark.png | Bin 0 -> 18385 bytes .../Resources/Logo_Twitter_Light.png | Bin 0 -> 19901 bytes src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj | 68 + .../SystemInfo/LinuxSystemInfo.cs | 85 ++ .../SystemInfo/MacOSSystemInfo.cs | 164 ++ src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs | 79 + .../SystemInfo/WindowsSystemInfo.cs | 87 ++ src/Ryujinx.UI.Common/UserError.cs | 39 + 64 files changed, 5524 insertions(+) create mode 100644 src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs create mode 100644 src/Ryujinx.UI.Common/App/ApplicationCountUpdatedEventArgs.cs create mode 100644 src/Ryujinx.UI.Common/App/ApplicationData.cs create mode 100644 src/Ryujinx.UI.Common/App/ApplicationJsonSerializerContext.cs create mode 100644 src/Ryujinx.UI.Common/App/ApplicationLibrary.cs create mode 100644 src/Ryujinx.UI.Common/App/ApplicationMetadata.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/AudioBackend.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormatSettings.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationJsonSerializerContext.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/FileTypes.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/LoggerModule.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/System/Language.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/System/Region.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/UI/ColumnSort.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/UI/ShownFileTypes.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/UI/WindowStartup.cs create mode 100644 src/Ryujinx.UI.Common/DiscordIntegrationModule.cs create mode 100644 src/Ryujinx.UI.Common/Extensions/FileTypeExtensions.cs create mode 100644 src/Ryujinx.UI.Common/Helper/CommandLineState.cs create mode 100644 src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/LinuxHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/ObjectiveC.cs create mode 100644 src/Ryujinx.UI.Common/Helper/OpenHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/SetupValidator.cs create mode 100644 src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/TitleHelper.cs create mode 100644 src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs create mode 100644 src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs create mode 100644 src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs create mode 100644 src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiUsage.cs create mode 100644 src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs create mode 100644 src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs create mode 100644 src/Ryujinx.UI.Common/Models/Github/GithubReleaseAssetJsonResponse.cs create mode 100644 src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs create mode 100644 src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonSerializerContext.cs create mode 100644 src/Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg create mode 100644 src/Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg create mode 100644 src/Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg create mode 100644 src/Ryujinx.UI.Common/Resources/Controller_ProCon.svg create mode 100644 src/Ryujinx.UI.Common/Resources/Icon_NCA.png create mode 100644 src/Ryujinx.UI.Common/Resources/Icon_NRO.png create mode 100644 src/Ryujinx.UI.Common/Resources/Icon_NSO.png create mode 100644 src/Ryujinx.UI.Common/Resources/Icon_NSP.png create mode 100644 src/Ryujinx.UI.Common/Resources/Icon_XCI.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Discord_Dark.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Discord_Light.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_GitHub_Dark.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_GitHub_Light.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Patreon_Dark.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Patreon_Light.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Twitter_Dark.png create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Twitter_Light.png create mode 100644 src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj create mode 100644 src/Ryujinx.UI.Common/SystemInfo/LinuxSystemInfo.cs create mode 100644 src/Ryujinx.UI.Common/SystemInfo/MacOSSystemInfo.cs create mode 100644 src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs create mode 100644 src/Ryujinx.UI.Common/SystemInfo/WindowsSystemInfo.cs create mode 100644 src/Ryujinx.UI.Common/UserError.cs (limited to 'src/Ryujinx.UI.Common') diff --git a/src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs b/src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs new file mode 100644 index 00000000..58e066b9 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ryujinx.UI.App.Common +{ + public class ApplicationAddedEventArgs : EventArgs + { + public ApplicationData AppData { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/ApplicationCountUpdatedEventArgs.cs b/src/Ryujinx.UI.Common/App/ApplicationCountUpdatedEventArgs.cs new file mode 100644 index 00000000..5ed7baf1 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationCountUpdatedEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.UI.App.Common +{ + public class ApplicationCountUpdatedEventArgs : EventArgs + { + public int NumAppsFound { get; set; } + public int NumAppsLoaded { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs new file mode 100644 index 00000000..8cc7238e --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -0,0 +1,158 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Loader; +using LibHac.Ns; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.Common.Helper; +using System; +using System.IO; + +namespace Ryujinx.UI.App.Common +{ + public class ApplicationData + { + public bool Favorite { get; set; } + public byte[] Icon { get; set; } + public string TitleName { get; set; } + public string TitleId { get; set; } + public string Developer { get; set; } + public string Version { get; set; } + public TimeSpan TimePlayed { get; set; } + public DateTime? LastPlayed { get; set; } + public string FileExtension { get; set; } + public long FileSize { get; set; } + public string Path { get; set; } + public BlitStruct ControlHolder { get; set; } + + public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); + + public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed); + + public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); + + public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) + { + using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); + + Nca mainNca = null; + Nca patchNca = null; + + if (!System.IO.Path.Exists(titleFilePath)) + { + Logger.Error?.Print(LogClass.Application, $"File does not exists. {titleFilePath}"); + return string.Empty; + } + + string extension = System.IO.Path.GetExtension(titleFilePath).ToLower(); + + if (extension is ".nsp" or ".xci") + { + IFileSystem pfs; + + if (extension == ".xci") + { + Xci xci = new(virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + 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 != NcaContentType.Program) + { + continue; + } + + 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"); + + return string.Empty; + } + + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + + if (updatePatchNca != null) + { + patchNca = updatePatchNca; + } + + IFileSystem codeFs = null; + + if (patchNca == null) + { + if (mainNca.CanOpenSection(NcaSectionType.Code)) + { + codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid); + } + } + else + { + if (patchNca.CanOpenSection(NcaSectionType.Code)) + { + codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid); + } + } + + if (codeFs == null) + { + Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA"); + + return string.Empty; + } + + const string MainExeFs = "main"; + + if (!codeFs.FileExists($"/{MainExeFs}")) + { + Logger.Error?.Print(LogClass.Loader, "No main binary ExeFS found in ExeFS"); + + return string.Empty; + } + + using var nsoFile = new UniqueRef(); + + codeFs.OpenFile(ref nsoFile.Ref, $"/{MainExeFs}".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + NsoReader reader = new(); + reader.Initialize(nsoFile.Release().AsStorage().AsFile(OpenMode.Read)).ThrowIfFailure(); + + return BitConverter.ToString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", "").ToUpper()[..16]; + } + } +} diff --git a/src/Ryujinx.UI.Common/App/ApplicationJsonSerializerContext.cs b/src/Ryujinx.UI.Common/App/ApplicationJsonSerializerContext.cs new file mode 100644 index 00000000..ada7cc34 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ApplicationMetadata))] + internal partial class ApplicationJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs new file mode 100644 index 00000000..65cf7a9e --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -0,0 +1,930 @@ +using LibHac; +using LibHac.Common; +using LibHac.Common.Keys; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.SystemState; +using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Configuration.System; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading; +using Path = System.IO.Path; +using TimeSpan = System.TimeSpan; + +namespace Ryujinx.UI.App.Common +{ + public class ApplicationLibrary + { + public event EventHandler ApplicationAdded; + public event EventHandler ApplicationCountUpdated; + + private readonly byte[] _nspIcon; + private readonly byte[] _xciIcon; + private readonly byte[] _ncaIcon; + private readonly byte[] _nroIcon; + private readonly byte[] _nsoIcon; + + private readonly VirtualFileSystem _virtualFileSystem; + private Language _desiredTitleLanguage; + private CancellationTokenSource _cancellationToken; + + private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) + { + _virtualFileSystem = virtualFileSystem; + + _nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png"); + _xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png"); + _ncaIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NCA.png"); + _nroIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NRO.png"); + _nsoIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSO.png"); + } + + private static byte[] GetResourceBytes(string resourceName) + { + Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); + byte[] resourceByteArray = new byte[resourceStream.Length]; + + resourceStream.Read(resourceByteArray); + + return resourceByteArray; + } + + public void CancelLoading() + { + _cancellationToken?.Cancel(); + } + + public static void ReadControlData(IFileSystem controlFs, Span outProperty) + { + using UniqueRef controlFile = new(); + + controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); + } + + public void LoadApplications(List appDirs, Language desiredTitleLanguage) + { + int numApplicationsFound = 0; + int numApplicationsLoaded = 0; + + _desiredTitleLanguage = desiredTitleLanguage; + + _cancellationToken = new CancellationTokenSource(); + + // Builds the applications list with paths to found applications + List applications = new(); + + try + { + foreach (string appDir in appDirs) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"{ReleaseInformation.ConfigName}\" contains an invalid directory: \"{appDir}\""); + + continue; + } + + try + { + IEnumerable files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file => + { + return + (Path.GetExtension(file).ToLower() is ".nsp" && ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value) || + (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value) || + (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value) || + (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value) || + (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value); + }); + + foreach (string app in files) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + var fileInfo = new FileInfo(app); + string extension = fileInfo.Extension.ToLower(); + + if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") + { + var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; + + if (!File.Exists(fullPath)) + { + Logger.Warning?.Print(LogClass.Application, $"Skipping invalid symlink: {fileInfo.FullName}"); + continue; + } + + applications.Add(fullPath); + numApplicationsFound++; + } + } + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\""); + } + } + + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applications) + { + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } + + long fileSize = new FileInfo(applicationPath).Length; + string titleName = "Unknown"; + string titleId = "0000000000000000"; + string developer = "Unknown"; + string version = "0"; + byte[] applicationIcon = null; + + BlitStruct controlHolder = new(1); + + try + { + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + { + try + { + IFileSystem pfs; + + bool isExeFs = false; + + if (extension == ".xci") + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; + + continue; + } + } + + if (isExeFs) + { + applicationIcon = _nspIcon; + + using UniqueRef npdmFile = new(); + + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + titleName = npdm.TitleName; + titleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); + + // Check if there is an update available. + if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + + if (applicationIcon != null) + { + break; + } + } + + applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + } + } + catch (MissingKeyException exception) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nro") + { + BinaryReader reader = new(file); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + applicationIcon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); + } + else + { + applicationIcon = _nroIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nca") + { + try + { + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + numApplicationsFound--; + + continue; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + // If its an NSO we just set defaults + else if (extension == ".nso") + { + applicationIcon = _nsoIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + numApplicationsFound--; + + continue; + } + + ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => + { + appMetadata.Title = titleName; + + // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. + if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) + { + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; + } + + // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) + { + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } + + } + }); + + ApplicationData data = new() + { + Favorite = appMetadata.Favorite, + Icon = applicationIcon, + TitleName = titleName, + TitleId = titleId, + Developer = developer, + Version = version, + TimePlayed = appMetadata.TimePlayed, + LastPlayed = appMetadata.LastPlayed, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, + Path = applicationPath, + ControlHolder = controlHolder, + }; + + numApplicationsLoaded++; + + OnApplicationAdded(new ApplicationAddedEventArgs + { + AppData = data, + }); + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs + { + NumAppsFound = numApplicationsFound, + NumAppsLoaded = numApplicationsLoaded, + }); + } + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs + { + NumAppsFound = numApplicationsFound, + NumAppsLoaded = numApplicationsLoaded, + }); + } + finally + { + _cancellationToken.Dispose(); + _cancellationToken = null; + } + } + + protected void OnApplicationAdded(ApplicationAddedEventArgs e) + { + ApplicationAdded?.Invoke(null, e); + } + + protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) + { + ApplicationCountUpdated?.Invoke(null, e); + } + + private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) + { + (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + + public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + { + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); + string metadataFile = Path.Combine(metadataFolder, "metadata.json"); + + ApplicationMetadata appMetadata; + + if (!File.Exists(metadataFile)) + { + Directory.CreateDirectory(metadataFolder); + + appMetadata = new ApplicationMetadata(); + + JsonHelper.SerializeToFile(metadataFile, appMetadata, _serializerContext.ApplicationMetadata); + } + + try + { + appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); + } + catch (JsonException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); + + appMetadata = new ApplicationMetadata(); + } + + if (modifyFunction != null) + { + modifyFunction(appMetadata); + + JsonHelper.SerializeToFile(metadataFile, appMetadata, _serializerContext.ApplicationMetadata); + } + + return appMetadata; + } + + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) + { + byte[] applicationIcon = null; + + try + { + // Look for icon only if applicationPath is not a directory + if (!Directory.Exists(applicationPath)) + { + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + { + try + { + IFileSystem pfs; + + bool isExeFs = false; + + if (extension == ".xci") + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + } + + if (isExeFs) + { + applicationIcon = _nspIcon; + } + else + { + // Store the ControlFS in variable called controlFs + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using (MemoryStream stream = new()) + { + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + + if (applicationIcon != null) + { + break; + } + } + + applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + } + } + catch (MissingKeyException) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + } + catch (InvalidDataException) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); + } + } + else if (extension == ".nro") + { + BinaryReader reader = new(file); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + applicationIcon = _nroIcon; + } + } + else + { + applicationIcon = _nroIcon; + } + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + } + } + else if (extension == ".nca") + { + applicationIcon = _ncaIcon; + } + // If its an NSO we just set defaults + else if (extension == ".nso") + { + applicationIcon = _nsoIcon; + } + } + } + catch (Exception) + { + Logger.Warning?.Print(LogClass.Application, $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}"); + } + + return applicationIcon ?? _ncaIcon; + } + + private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) + { + _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + + if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) + { + titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + } + else + { + titleName = null; + publisher = null; + } + + if (string.IsNullOrWhiteSpace(titleName)) + { + foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) + { + if (!controlTitle.NameString.IsEmpty()) + { + titleName = controlTitle.NameString.ToString(); + + break; + } + } + } + + if (string.IsNullOrWhiteSpace(publisher)) + { + foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) + { + if (!controlTitle.PublisherString.IsEmpty()) + { + publisher = controlTitle.PublisherString.ToString(); + + break; + } + } + } + + if (controlData.PresenceGroupId != 0) + { + titleId = controlData.PresenceGroupId.ToString("x16"); + } + else if (controlData.SaveDataOwnerId != 0) + { + titleId = controlData.SaveDataOwnerId.ToString(); + } + else if (controlData.AddOnContentBaseId != 0) + { + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; + } + + version = controlData.DisplayVersionString.ToString(); + } + + private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) + { + updatedControlFs = null; + + string updatePath = "(unknown)"; + + try + { + (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); + + if (patchNca != null && controlNca != null) + { + updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + + return true; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); + } + + return false; + } + + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) + { + Nca mainNca = null; + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + 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(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (mainNca, patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + { + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + 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(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) + { + updatePath = null; + + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } + } +} diff --git a/src/Ryujinx.UI.Common/App/ApplicationMetadata.cs b/src/Ryujinx.UI.Common/App/ApplicationMetadata.cs new file mode 100644 index 00000000..81193c5b --- /dev/null +++ b/src/Ryujinx.UI.Common/App/ApplicationMetadata.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + public class ApplicationMetadata + { + public string Title { get; set; } + public bool Favorite { get; set; } + + [JsonPropertyName("timespan_played")] + public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero; + + [JsonPropertyName("last_played_utc")] + public DateTime? LastPlayed { get; set; } = null; + + [JsonPropertyName("time_played")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TimePlayedOld { get; set; } + + [JsonPropertyName("last_played")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string LastPlayedOld { get; set; } + + /// + /// Updates . Call this before launching a game. + /// + public void UpdatePreGame() + { + LastPlayed = DateTime.UtcNow; + } + + /// + /// Updates and . Call this after a game ends. + /// + public void UpdatePostGame() + { + DateTime? prevLastPlayed = LastPlayed; + UpdatePreGame(); + + if (!prevLastPlayed.HasValue) + { + return; + } + + TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; + double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds; + TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/AudioBackend.cs b/src/Ryujinx.UI.Common/Configuration/AudioBackend.cs new file mode 100644 index 00000000..a952e7ac --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/AudioBackend.cs @@ -0,0 +1,14 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Configuration +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + public enum AudioBackend + { + Dummy, + OpenAl, + SoundIo, + SDL2, + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs new file mode 100644 index 00000000..0ee51d83 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -0,0 +1,409 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace Ryujinx.UI.Common.Configuration +{ + public class ConfigurationFileFormat + { + /// + /// The current version of the file format + /// + public const int CurrentVersion = 48; + + /// + /// Version of the configuration file format + /// + public int Version { get; set; } + + /// + /// Enables or disables logging to a file on disk + /// + public bool EnableFileLog { get; set; } + + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public BackendThreading BackendThreading { get; set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public int ResScale { get; set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public float ResScaleCustom { get; set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public float MaxAnisotropy { get; set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public AspectRatio AspectRatio { get; set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public AntiAliasing AntiAliasing { get; set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ScalingFilter ScalingFilter { get; set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public int ScalingFilterLevel { get; set; } + + /// + /// Dumps shaders in this local directory + /// + public string GraphicsShadersDumpPath { get; set; } + + /// + /// Enables printing debug log messages + /// + public bool LoggingEnableDebug { get; set; } + + /// + /// Enables printing stub log messages + /// + public bool LoggingEnableStub { get; set; } + + /// + /// Enables printing info log messages + /// + public bool LoggingEnableInfo { get; set; } + + /// + /// Enables printing warning log messages + /// + public bool LoggingEnableWarn { get; set; } + + /// + /// Enables printing error log messages + /// + public bool LoggingEnableError { get; set; } + + /// + /// Enables printing trace log messages + /// + public bool LoggingEnableTrace { get; set; } + + /// + /// Enables printing guest log messages + /// + public bool LoggingEnableGuest { get; set; } + + /// + /// Enables printing FS access log messages + /// + public bool LoggingEnableFsAccessLog { get; set; } + + /// + /// Controls which log messages are written to the log targets + /// + public LogClass[] LoggingFilteredClasses { get; set; } + + /// + /// Change Graphics API debug log level + /// + public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; } + + /// + /// Change System Language + /// + public Language SystemLanguage { get; set; } + + /// + /// Change System Region + /// + public Region SystemRegion { get; set; } + + /// + /// Change System TimeZone + /// + public string SystemTimeZone { get; set; } + + /// + /// Change System Time Offset in seconds + /// + public long SystemTimeOffset { get; set; } + + /// + /// Enables or disables Docked Mode + /// + public bool DockedMode { get; set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public bool EnableDiscordIntegration { get; set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public bool CheckUpdatesOnStart { get; set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public bool ShowConfirmExit { get; set; } + + /// + /// Whether to hide cursor on idle, always or never + /// + public HideCursorMode HideCursor { get; set; } + + /// + /// Enables or disables Vertical Sync + /// + public bool EnableVsync { get; set; } + + /// + /// Enables or disables Shader cache + /// + public bool EnableShaderCache { get; set; } + + /// + /// Enables or disables texture recompression + /// + public bool EnableTextureRecompression { get; set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public bool EnableMacroHLE { get; set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public bool EnableColorSpacePassthrough { get; set; } + + /// + /// Enables or disables profiled translation cache persistency + /// + public bool EnablePtc { get; set; } + + /// + /// Enables or disables guest Internet access + /// + public bool EnableInternetAccess { get; set; } + + /// + /// Enables integrity checks on Game content files + /// + public bool EnableFsIntegrityChecks { get; set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public int FsGlobalAccessLogMode { get; set; } + + /// + /// The selected audio backend + /// + public AudioBackend AudioBackend { get; set; } + + /// + /// The audio volume + /// + public float AudioVolume { get; set; } + + /// + /// The selected memory manager mode + /// + public MemoryManagerMode MemoryManagerMode { get; set; } + + /// + /// Expands the RAM amount on the emulated system from 4GiB to 6GiB + /// + public bool ExpandRam { get; set; } + + /// + /// Enable or disable ignoring missing services + /// + public bool IgnoreMissingServices { get; set; } + + /// + /// Used to toggle columns in the GUI + /// + public GuiColumns GuiColumns { get; set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSort ColumnSort { get; set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public List GameDirs { get; set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypes ShownFileTypes { get; set; } + + /// + /// Main window start-up position, size and state + /// + public WindowStartup WindowStartup { get; set; } + + /// + /// Language Code for the UI + /// + public string LanguageCode { get; set; } + + /// + /// Enable or disable custom themes in the GUI + /// + public bool EnableCustomTheme { get; set; } + + /// + /// Path to custom GUI theme + /// + public string CustomThemePath { get; set; } + + /// + /// Chooses the base style // Not Used + /// + public string BaseStyle { get; set; } + + /// + /// Chooses the view mode of the game list // Not Used + /// + public int GameListViewMode { get; set; } + + /// + /// Show application name in Grid Mode // Not Used + /// + public bool ShowNames { get; set; } + + /// + /// Sets App Icon Size // Not Used + /// + public int GridSize { get; set; } + + /// + /// Sorts Apps in the game list // Not Used + /// + public int ApplicationSort { get; set; } + + /// + /// Sets if Grid is ordered in Ascending Order // Not Used + /// + public bool IsAscendingOrder { get; set; } + + /// + /// Start games in fullscreen mode + /// + public bool StartFullscreen { get; set; } + + /// + /// Show console window + /// + public bool ShowConsole { get; set; } + + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public bool EnableKeyboard { get; set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public bool EnableMouse { get; set; } + + /// + /// Hotkey Keyboard Bindings + /// + public KeyboardHotkeys Hotkeys { get; set; } + + /// + /// Legacy keyboard control bindings + /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. + public List KeyboardConfig { get; set; } + + /// + /// Legacy controller control bindings + /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. + public List ControllerConfig { get; set; } + + /// + /// Input configurations + /// + public List InputConfig { get; set; } + + /// + /// Graphics backend + /// + public GraphicsBackend GraphicsBackend { get; set; } + + /// + /// Preferred GPU + /// + public string PreferredGpu { get; set; } + + /// + /// Multiplayer Mode + /// + public MultiplayerMode MultiplayerMode { get; set; } + + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public string MultiplayerLanInterfaceId { get; set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public bool UseHypervisor { get; set; } + + /// + /// Loads a configuration file from disk + /// + /// The path to the JSON configuration file + /// Parsed configuration file + public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat) + { + try + { + configurationFileFormat = JsonHelper.DeserializeFromFile(path, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat); + + return configurationFileFormat.Version != 0; + } + catch + { + configurationFileFormat = null; + + return false; + } + } + + /// + /// Save a configuration file to disk + /// + /// The path to the JSON configuration file + public void SaveConfig(string path) + { + JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormatSettings.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormatSettings.cs new file mode 100644 index 00000000..9861ebf1 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormatSettings.cs @@ -0,0 +1,9 @@ +using Ryujinx.Common.Utilities; + +namespace Ryujinx.UI.Common.Configuration +{ + internal static class ConfigurationFileFormatSettings + { + public static readonly ConfigurationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationJsonSerializerContext.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationJsonSerializerContext.cs new file mode 100644 index 00000000..3c3e3f20 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ConfigurationFileFormat))] + internal partial class ConfigurationJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs new file mode 100644 index 00000000..1d6934ce --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -0,0 +1,1562 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Vulkan; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using Ryujinx.UI.Common.Helper; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace Ryujinx.UI.Common.Configuration +{ + public class ConfigurationState + { + /// + /// UI configuration section + /// + public class UISection + { + public class Columns + { + public ReactiveObject FavColumn { get; private set; } + public ReactiveObject IconColumn { get; private set; } + public ReactiveObject AppColumn { get; private set; } + public ReactiveObject DevColumn { get; private set; } + public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject TimePlayedColumn { get; private set; } + public ReactiveObject LastPlayedColumn { get; private set; } + public ReactiveObject FileExtColumn { get; private set; } + public ReactiveObject FileSizeColumn { get; private set; } + public ReactiveObject PathColumn { get; private set; } + + public Columns() + { + FavColumn = new ReactiveObject(); + IconColumn = new ReactiveObject(); + AppColumn = new ReactiveObject(); + DevColumn = new ReactiveObject(); + VersionColumn = new ReactiveObject(); + TimePlayedColumn = new ReactiveObject(); + LastPlayedColumn = new ReactiveObject(); + FileExtColumn = new ReactiveObject(); + FileSizeColumn = new ReactiveObject(); + PathColumn = new ReactiveObject(); + } + } + + public class ColumnSortSettings + { + public ReactiveObject SortColumnId { get; private set; } + public ReactiveObject SortAscending { get; private set; } + + public ColumnSortSettings() + { + SortColumnId = new ReactiveObject(); + SortAscending = new ReactiveObject(); + } + } + + /// + /// Used to toggle which file types are shown in the UI + /// + public class ShownFileTypeSettings + { + public ReactiveObject NSP { get; private set; } + public ReactiveObject PFS0 { get; private set; } + public ReactiveObject XCI { get; private set; } + public ReactiveObject NCA { get; private set; } + public ReactiveObject NRO { get; private set; } + public ReactiveObject NSO { get; private set; } + + public ShownFileTypeSettings() + { + NSP = new ReactiveObject(); + PFS0 = new ReactiveObject(); + XCI = new ReactiveObject(); + NCA = new ReactiveObject(); + NRO = new ReactiveObject(); + NSO = new ReactiveObject(); + } + } + + // + /// Determines main window start-up position, size and state + /// + public class WindowStartupSettings + { + public ReactiveObject WindowSizeWidth { get; private set; } + public ReactiveObject WindowSizeHeight { get; private set; } + public ReactiveObject WindowPositionX { get; private set; } + public ReactiveObject WindowPositionY { get; private set; } + public ReactiveObject WindowMaximized { get; private set; } + + public WindowStartupSettings() + { + WindowSizeWidth = new ReactiveObject(); + WindowSizeHeight = new ReactiveObject(); + WindowPositionX = new ReactiveObject(); + WindowPositionY = new ReactiveObject(); + WindowMaximized = new ReactiveObject(); + } + } + + /// + /// Used to toggle columns in the GUI + /// + public Columns GuiColumns { get; private set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSortSettings ColumnSort { get; private set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypeSettings ShownFileTypes { get; private set; } + + /// + /// Determines main window start-up position, size and state + /// + public WindowStartupSettings WindowStartup { get; private set; } + + /// + /// Language Code for the UI + /// + public ReactiveObject LanguageCode { get; private set; } + + /// + /// Enable or disable custom themes in the GUI + /// + public ReactiveObject EnableCustomTheme { get; private set; } + + /// + /// Path to custom GUI theme + /// + public ReactiveObject CustomThemePath { get; private set; } + + /// + /// Selects the base style + /// + public ReactiveObject BaseStyle { get; private set; } + + /// + /// Start games in fullscreen mode + /// + public ReactiveObject StartFullscreen { get; private set; } + + /// + /// Hide / Show Console Window + /// + public ReactiveObject ShowConsole { get; private set; } + + /// + /// View Mode of the Game list + /// + public ReactiveObject GameListViewMode { get; private set; } + + /// + /// Show application name in Grid Mode + /// + public ReactiveObject ShowNames { get; private set; } + + /// + /// Sets App Icon Size in Grid Mode + /// + public ReactiveObject GridSize { get; private set; } + + /// + /// Sorts Apps in Grid Mode + /// + public ReactiveObject ApplicationSort { get; private set; } + + /// + /// Sets if Grid is ordered in Ascending Order + /// + public ReactiveObject IsAscendingOrder { get; private set; } + + public UISection() + { + GuiColumns = new Columns(); + ColumnSort = new ColumnSortSettings(); + GameDirs = new ReactiveObject>(); + ShownFileTypes = new ShownFileTypeSettings(); + WindowStartup = new WindowStartupSettings(); + EnableCustomTheme = new ReactiveObject(); + CustomThemePath = new ReactiveObject(); + BaseStyle = new ReactiveObject(); + StartFullscreen = new ReactiveObject(); + GameListViewMode = new ReactiveObject(); + ShowNames = new ReactiveObject(); + GridSize = new ReactiveObject(); + ApplicationSort = new ReactiveObject(); + IsAscendingOrder = new ReactiveObject(); + LanguageCode = new ReactiveObject(); + ShowConsole = new ReactiveObject(); + ShowConsole.Event += static (s, e) => { ConsoleHelper.SetConsoleWindowState(e.NewValue); }; + } + } + + /// + /// Logger configuration section + /// + public class LoggerSection + { + /// + /// Enables printing debug log messages + /// + public ReactiveObject EnableDebug { get; private set; } + + /// + /// Enables printing stub log messages + /// + public ReactiveObject EnableStub { get; private set; } + + /// + /// Enables printing info log messages + /// + public ReactiveObject EnableInfo { get; private set; } + + /// + /// Enables printing warning log messages + /// + public ReactiveObject EnableWarn { get; private set; } + + /// + /// Enables printing error log messages + /// + public ReactiveObject EnableError { get; private set; } + + /// + /// Enables printing trace log messages + /// + public ReactiveObject EnableTrace { get; private set; } + + /// + /// Enables printing guest log messages + /// + public ReactiveObject EnableGuest { get; private set; } + + /// + /// Enables printing FS access log messages + /// + public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Controls which log messages are written to the log targets + /// + public ReactiveObject FilteredClasses { get; private set; } + + /// + /// Enables or disables logging to a file on disk + /// + public ReactiveObject EnableFileLog { get; private set; } + + /// + /// Controls which OpenGL log messages are recorded in the log + /// + public ReactiveObject GraphicsDebugLevel { get; private set; } + + public LoggerSection() + { + EnableDebug = new ReactiveObject(); + EnableStub = new ReactiveObject(); + EnableInfo = new ReactiveObject(); + EnableWarn = new ReactiveObject(); + EnableError = new ReactiveObject(); + EnableTrace = new ReactiveObject(); + EnableGuest = new ReactiveObject(); + EnableFsAccessLog = new ReactiveObject(); + FilteredClasses = new ReactiveObject(); + EnableFileLog = new ReactiveObject(); + EnableFileLog.Event += static (sender, e) => LogValueChange(e, nameof(EnableFileLog)); + GraphicsDebugLevel = new ReactiveObject(); + } + } + + /// + /// System configuration section + /// + public class SystemSection + { + /// + /// Change System Language + /// + public ReactiveObject Language { get; private set; } + + /// + /// Change System Region + /// + public ReactiveObject Region { get; private set; } + + /// + /// Change System TimeZone + /// + public ReactiveObject TimeZone { get; private set; } + + /// + /// System Time Offset in Seconds + /// + public ReactiveObject SystemTimeOffset { get; private set; } + + /// + /// Enables or disables Docked Mode + /// + public ReactiveObject EnableDockedMode { get; private set; } + + /// + /// Enables or disables profiled translation cache persistency + /// + public ReactiveObject EnablePtc { get; private set; } + + /// + /// Enables or disables guest Internet access + /// + public ReactiveObject EnableInternetAccess { get; private set; } + + /// + /// Enables integrity checks on Game content files + /// + public ReactiveObject EnableFsIntegrityChecks { get; private set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public ReactiveObject FsGlobalAccessLogMode { get; private set; } + + /// + /// The selected audio backend + /// + public ReactiveObject AudioBackend { get; private set; } + + /// + /// The audio backend volume + /// + public ReactiveObject AudioVolume { get; private set; } + + /// + /// The selected memory manager mode + /// + public ReactiveObject MemoryManagerMode { get; private set; } + + /// + /// Defines the amount of RAM available on the emulated system, and how it is distributed + /// + public ReactiveObject ExpandRam { get; private set; } + + /// + /// Enable or disable ignoring missing services + /// + public ReactiveObject IgnoreMissingServices { get; private set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public ReactiveObject UseHypervisor { get; private set; } + + public SystemSection() + { + Language = new ReactiveObject(); + Region = new ReactiveObject(); + TimeZone = new ReactiveObject(); + SystemTimeOffset = new ReactiveObject(); + EnableDockedMode = new ReactiveObject(); + EnableDockedMode.Event += static (sender, e) => LogValueChange(e, nameof(EnableDockedMode)); + EnablePtc = new ReactiveObject(); + EnablePtc.Event += static (sender, e) => LogValueChange(e, nameof(EnablePtc)); + EnableInternetAccess = new ReactiveObject(); + EnableInternetAccess.Event += static (sender, e) => LogValueChange(e, nameof(EnableInternetAccess)); + EnableFsIntegrityChecks = new ReactiveObject(); + EnableFsIntegrityChecks.Event += static (sender, e) => LogValueChange(e, nameof(EnableFsIntegrityChecks)); + FsGlobalAccessLogMode = new ReactiveObject(); + FsGlobalAccessLogMode.Event += static (sender, e) => LogValueChange(e, nameof(FsGlobalAccessLogMode)); + AudioBackend = new ReactiveObject(); + AudioBackend.Event += static (sender, e) => LogValueChange(e, nameof(AudioBackend)); + MemoryManagerMode = new ReactiveObject(); + MemoryManagerMode.Event += static (sender, e) => LogValueChange(e, nameof(MemoryManagerMode)); + ExpandRam = new ReactiveObject(); + ExpandRam.Event += static (sender, e) => LogValueChange(e, nameof(ExpandRam)); + IgnoreMissingServices = new ReactiveObject(); + IgnoreMissingServices.Event += static (sender, e) => LogValueChange(e, nameof(IgnoreMissingServices)); + AudioVolume = new ReactiveObject(); + AudioVolume.Event += static (sender, e) => LogValueChange(e, nameof(AudioVolume)); + UseHypervisor = new ReactiveObject(); + UseHypervisor.Event += static (sender, e) => LogValueChange(e, nameof(UseHypervisor)); + } + } + + /// + /// Hid configuration section + /// + public class HidSection + { + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } + + /// + /// Hotkey Keyboard Bindings + /// + public ReactiveObject Hotkeys { get; private set; } + + /// + /// Input device configuration. + /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. + /// TODO: Implement a ReactiveList class. + /// + public ReactiveObject> InputConfig { get; private set; } + + public HidSection() + { + EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); + Hotkeys = new ReactiveObject(); + InputConfig = new ReactiveObject>(); + } + } + + /// + /// Graphics configuration section + /// + public class GraphicsSection + { + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public ReactiveObject BackendThreading { get; private set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public ReactiveObject MaxAnisotropy { get; private set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public ReactiveObject AspectRatio { get; private set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public ReactiveObject ResScale { get; private set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public ReactiveObject ResScaleCustom { get; private set; } + + /// + /// Dumps shaders in this local directory + /// + public ReactiveObject ShadersDumpPath { get; private set; } + + /// + /// Enables or disables Vertical Sync + /// + public ReactiveObject EnableVsync { get; private set; } + + /// + /// Enables or disables Shader cache + /// + public ReactiveObject EnableShaderCache { get; private set; } + + /// + /// Enables or disables texture recompression + /// + public ReactiveObject EnableTextureRecompression { get; private set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public ReactiveObject EnableMacroHLE { get; private set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + + /// + /// Graphics backend + /// + public ReactiveObject GraphicsBackend { get; private set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public ReactiveObject AntiAliasing { get; private set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ReactiveObject ScalingFilter { get; private set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public ReactiveObject ScalingFilterLevel { get; private set; } + + /// + /// Preferred GPU + /// + public ReactiveObject PreferredGpu { get; private set; } + + public GraphicsSection() + { + BackendThreading = new ReactiveObject(); + BackendThreading.Event += static (sender, e) => LogValueChange(e, nameof(BackendThreading)); + ResScale = new ReactiveObject(); + ResScale.Event += static (sender, e) => LogValueChange(e, nameof(ResScale)); + ResScaleCustom = new ReactiveObject(); + ResScaleCustom.Event += static (sender, e) => LogValueChange(e, nameof(ResScaleCustom)); + MaxAnisotropy = new ReactiveObject(); + MaxAnisotropy.Event += static (sender, e) => LogValueChange(e, nameof(MaxAnisotropy)); + AspectRatio = new ReactiveObject(); + AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); + ShadersDumpPath = new ReactiveObject(); + EnableVsync = new ReactiveObject(); + EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); + EnableShaderCache = new ReactiveObject(); + EnableShaderCache.Event += static (sender, e) => LogValueChange(e, nameof(EnableShaderCache)); + EnableTextureRecompression = new ReactiveObject(); + EnableTextureRecompression.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureRecompression)); + GraphicsBackend = new ReactiveObject(); + GraphicsBackend.Event += static (sender, e) => LogValueChange(e, nameof(GraphicsBackend)); + PreferredGpu = new ReactiveObject(); + PreferredGpu.Event += static (sender, e) => LogValueChange(e, nameof(PreferredGpu)); + EnableMacroHLE = new ReactiveObject(); + EnableMacroHLE.Event += static (sender, e) => LogValueChange(e, nameof(EnableMacroHLE)); + EnableColorSpacePassthrough = new ReactiveObject(); + EnableColorSpacePassthrough.Event += static (sender, e) => LogValueChange(e, nameof(EnableColorSpacePassthrough)); + AntiAliasing = new ReactiveObject(); + AntiAliasing.Event += static (sender, e) => LogValueChange(e, nameof(AntiAliasing)); + ScalingFilter = new ReactiveObject(); + ScalingFilter.Event += static (sender, e) => LogValueChange(e, nameof(ScalingFilter)); + ScalingFilterLevel = new ReactiveObject(); + ScalingFilterLevel.Event += static (sender, e) => LogValueChange(e, nameof(ScalingFilterLevel)); + } + } + + /// + /// Multiplayer configuration section + /// + public class MultiplayerSection + { + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public ReactiveObject LanInterfaceId { get; private set; } + + /// + /// Multiplayer Mode + /// + public ReactiveObject Mode { get; private set; } + + public MultiplayerSection() + { + LanInterfaceId = new ReactiveObject(); + Mode = new ReactiveObject(); + Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); + } + } + + /// + /// The default configuration instance + /// + public static ConfigurationState Instance { get; private set; } + + /// + /// The UI section + /// + public UISection UI { get; private set; } + + /// + /// The Logger section + /// + public LoggerSection Logger { get; private set; } + + /// + /// The System section + /// + public SystemSection System { get; private set; } + + /// + /// The Graphics section + /// + public GraphicsSection Graphics { get; private set; } + + /// + /// The Hid section + /// + public HidSection Hid { get; private set; } + + /// + /// The Multiplayer section + /// + public MultiplayerSection Multiplayer { get; private set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public ReactiveObject EnableDiscordIntegration { get; private set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public ReactiveObject CheckUpdatesOnStart { get; private set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public ReactiveObject ShowConfirmExit { get; private set; } + + /// + /// Hide Cursor on Idle + /// + public ReactiveObject HideCursor { get; private set; } + + private ConfigurationState() + { + UI = new UISection(); + Logger = new LoggerSection(); + System = new SystemSection(); + Graphics = new GraphicsSection(); + Hid = new HidSection(); + Multiplayer = new MultiplayerSection(); + EnableDiscordIntegration = new ReactiveObject(); + CheckUpdatesOnStart = new ReactiveObject(); + ShowConfirmExit = new ReactiveObject(); + HideCursor = new ReactiveObject(); + } + + public ConfigurationFileFormat ToFileFormat() + { + ConfigurationFileFormat configurationFile = new() + { + Version = ConfigurationFileFormat.CurrentVersion, + BackendThreading = Graphics.BackendThreading, + EnableFileLog = Logger.EnableFileLog, + ResScale = Graphics.ResScale, + ResScaleCustom = Graphics.ResScaleCustom, + MaxAnisotropy = Graphics.MaxAnisotropy, + AspectRatio = Graphics.AspectRatio, + AntiAliasing = Graphics.AntiAliasing, + ScalingFilter = Graphics.ScalingFilter, + ScalingFilterLevel = Graphics.ScalingFilterLevel, + GraphicsShadersDumpPath = Graphics.ShadersDumpPath, + LoggingEnableDebug = Logger.EnableDebug, + LoggingEnableStub = Logger.EnableStub, + LoggingEnableInfo = Logger.EnableInfo, + LoggingEnableWarn = Logger.EnableWarn, + LoggingEnableError = Logger.EnableError, + LoggingEnableTrace = Logger.EnableTrace, + LoggingEnableGuest = Logger.EnableGuest, + LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, + LoggingFilteredClasses = Logger.FilteredClasses, + LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, + SystemLanguage = System.Language, + SystemRegion = System.Region, + SystemTimeZone = System.TimeZone, + SystemTimeOffset = System.SystemTimeOffset, + DockedMode = System.EnableDockedMode, + EnableDiscordIntegration = EnableDiscordIntegration, + CheckUpdatesOnStart = CheckUpdatesOnStart, + ShowConfirmExit = ShowConfirmExit, + HideCursor = HideCursor, + EnableVsync = Graphics.EnableVsync, + EnableShaderCache = Graphics.EnableShaderCache, + EnableTextureRecompression = Graphics.EnableTextureRecompression, + EnableMacroHLE = Graphics.EnableMacroHLE, + EnableColorSpacePassthrough = Graphics.EnableColorSpacePassthrough, + EnablePtc = System.EnablePtc, + EnableInternetAccess = System.EnableInternetAccess, + EnableFsIntegrityChecks = System.EnableFsIntegrityChecks, + FsGlobalAccessLogMode = System.FsGlobalAccessLogMode, + AudioBackend = System.AudioBackend, + AudioVolume = System.AudioVolume, + MemoryManagerMode = System.MemoryManagerMode, + ExpandRam = System.ExpandRam, + IgnoreMissingServices = System.IgnoreMissingServices, + UseHypervisor = System.UseHypervisor, + GuiColumns = new GuiColumns + { + FavColumn = UI.GuiColumns.FavColumn, + IconColumn = UI.GuiColumns.IconColumn, + AppColumn = UI.GuiColumns.AppColumn, + DevColumn = UI.GuiColumns.DevColumn, + VersionColumn = UI.GuiColumns.VersionColumn, + TimePlayedColumn = UI.GuiColumns.TimePlayedColumn, + LastPlayedColumn = UI.GuiColumns.LastPlayedColumn, + FileExtColumn = UI.GuiColumns.FileExtColumn, + FileSizeColumn = UI.GuiColumns.FileSizeColumn, + PathColumn = UI.GuiColumns.PathColumn, + }, + ColumnSort = new ColumnSort + { + SortColumnId = UI.ColumnSort.SortColumnId, + SortAscending = UI.ColumnSort.SortAscending, + }, + GameDirs = UI.GameDirs, + ShownFileTypes = new ShownFileTypes + { + NSP = UI.ShownFileTypes.NSP, + PFS0 = UI.ShownFileTypes.PFS0, + XCI = UI.ShownFileTypes.XCI, + NCA = UI.ShownFileTypes.NCA, + NRO = UI.ShownFileTypes.NRO, + NSO = UI.ShownFileTypes.NSO, + }, + WindowStartup = new WindowStartup + { + WindowSizeWidth = UI.WindowStartup.WindowSizeWidth, + WindowSizeHeight = UI.WindowStartup.WindowSizeHeight, + WindowPositionX = UI.WindowStartup.WindowPositionX, + WindowPositionY = UI.WindowStartup.WindowPositionY, + WindowMaximized = UI.WindowStartup.WindowMaximized, + }, + LanguageCode = UI.LanguageCode, + EnableCustomTheme = UI.EnableCustomTheme, + CustomThemePath = UI.CustomThemePath, + BaseStyle = UI.BaseStyle, + GameListViewMode = UI.GameListViewMode, + ShowNames = UI.ShowNames, + GridSize = UI.GridSize, + ApplicationSort = UI.ApplicationSort, + IsAscendingOrder = UI.IsAscendingOrder, + StartFullscreen = UI.StartFullscreen, + ShowConsole = UI.ShowConsole, + EnableKeyboard = Hid.EnableKeyboard, + EnableMouse = Hid.EnableMouse, + Hotkeys = Hid.Hotkeys, + KeyboardConfig = new List(), + ControllerConfig = new List(), + InputConfig = Hid.InputConfig, + GraphicsBackend = Graphics.GraphicsBackend, + PreferredGpu = Graphics.PreferredGpu, + MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, + MultiplayerMode = Multiplayer.Mode, + }; + + return configurationFile; + } + + public void LoadDefault() + { + Logger.EnableFileLog.Value = true; + Graphics.BackendThreading.Value = BackendThreading.Auto; + Graphics.ResScale.Value = 1; + Graphics.ResScaleCustom.Value = 1.0f; + Graphics.MaxAnisotropy.Value = -1.0f; + Graphics.AspectRatio.Value = AspectRatio.Fixed16x9; + Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); + Graphics.PreferredGpu.Value = ""; + Graphics.ShadersDumpPath.Value = ""; + Logger.EnableDebug.Value = false; + Logger.EnableStub.Value = true; + Logger.EnableInfo.Value = true; + Logger.EnableWarn.Value = true; + Logger.EnableError.Value = true; + Logger.EnableTrace.Value = false; + Logger.EnableGuest.Value = true; + Logger.EnableFsAccessLog.Value = false; + Logger.FilteredClasses.Value = Array.Empty(); + Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; + System.Language.Value = Language.AmericanEnglish; + System.Region.Value = Region.USA; + System.TimeZone.Value = "UTC"; + System.SystemTimeOffset.Value = 0; + System.EnableDockedMode.Value = true; + EnableDiscordIntegration.Value = true; + CheckUpdatesOnStart.Value = true; + ShowConfirmExit.Value = true; + HideCursor.Value = HideCursorMode.OnIdle; + Graphics.EnableVsync.Value = true; + Graphics.EnableShaderCache.Value = true; + Graphics.EnableTextureRecompression.Value = false; + Graphics.EnableMacroHLE.Value = true; + Graphics.EnableColorSpacePassthrough.Value = false; + Graphics.AntiAliasing.Value = AntiAliasing.None; + Graphics.ScalingFilter.Value = ScalingFilter.Bilinear; + Graphics.ScalingFilterLevel.Value = 80; + System.EnablePtc.Value = true; + System.EnableInternetAccess.Value = false; + System.EnableFsIntegrityChecks.Value = true; + System.FsGlobalAccessLogMode.Value = 0; + System.AudioBackend.Value = AudioBackend.SDL2; + System.AudioVolume.Value = 1; + System.MemoryManagerMode.Value = MemoryManagerMode.HostMappedUnsafe; + System.ExpandRam.Value = false; + System.IgnoreMissingServices.Value = false; + System.UseHypervisor.Value = true; + Multiplayer.LanInterfaceId.Value = "0"; + Multiplayer.Mode.Value = MultiplayerMode.Disabled; + UI.GuiColumns.FavColumn.Value = true; + UI.GuiColumns.IconColumn.Value = true; + UI.GuiColumns.AppColumn.Value = true; + UI.GuiColumns.DevColumn.Value = true; + UI.GuiColumns.VersionColumn.Value = true; + UI.GuiColumns.TimePlayedColumn.Value = true; + UI.GuiColumns.LastPlayedColumn.Value = true; + UI.GuiColumns.FileExtColumn.Value = true; + UI.GuiColumns.FileSizeColumn.Value = true; + UI.GuiColumns.PathColumn.Value = true; + UI.ColumnSort.SortColumnId.Value = 0; + UI.ColumnSort.SortAscending.Value = false; + UI.GameDirs.Value = new List(); + UI.ShownFileTypes.NSP.Value = true; + UI.ShownFileTypes.PFS0.Value = true; + UI.ShownFileTypes.XCI.Value = true; + UI.ShownFileTypes.NCA.Value = true; + UI.ShownFileTypes.NRO.Value = true; + UI.ShownFileTypes.NSO.Value = true; + UI.EnableCustomTheme.Value = true; + UI.LanguageCode.Value = "en_US"; + UI.CustomThemePath.Value = ""; + UI.BaseStyle.Value = "Dark"; + UI.GameListViewMode.Value = 0; + UI.ShowNames.Value = true; + UI.GridSize.Value = 2; + UI.ApplicationSort.Value = 0; + UI.IsAscendingOrder.Value = true; + UI.StartFullscreen.Value = false; + UI.ShowConsole.Value = true; + UI.WindowStartup.WindowSizeWidth.Value = 1280; + UI.WindowStartup.WindowSizeHeight.Value = 760; + UI.WindowStartup.WindowPositionX.Value = 0; + UI.WindowStartup.WindowPositionY.Value = 0; + UI.WindowStartup.WindowMaximized.Value = false; + Hid.EnableKeyboard.Value = false; + Hid.EnableMouse.Value = false; + Hid.Hotkeys.Value = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + ToggleMute = Key.F2, + Screenshot = Key.F8, + ShowUI = Key.F4, + Pause = Key.F5, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + Hid.InputConfig.Value = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + ControllerType = ControllerType.JoyconPair, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }, + }; + } + + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + { + bool configurationFileUpdated = false; + + if (configurationFileFormat.Version < 0 || configurationFileFormat.Version > ConfigurationFileFormat.CurrentVersion) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); + + LoadDefault(); + } + + if (configurationFileFormat.Version < 2) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); + + configurationFileFormat.SystemRegion = Region.USA; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 3) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); + + configurationFileFormat.SystemTimeZone = "UTC"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 4) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); + + configurationFileFormat.MaxAnisotropy = -1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 5) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); + + configurationFileFormat.SystemTimeOffset = 0; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 8) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); + + configurationFileFormat.EnablePtc = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 9) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); + + configurationFileFormat.ColumnSort = new ColumnSort + { + SortColumnId = 0, + SortAscending = false, + }; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 10) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); + + configurationFileFormat.AudioBackend = AudioBackend.OpenAl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 11) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); + + configurationFileFormat.ResScale = 1; + configurationFileFormat.ResScaleCustom = 1.0f; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 12) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); + + configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 13 -> LDN1 + + if (configurationFileFormat.Version < 14) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); + + configurationFileFormat.CheckUpdatesOnStart = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 16) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); + + configurationFileFormat.EnableShaderCache = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 17) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); + + configurationFileFormat.StartFullscreen = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 18) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); + + configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 19 -> LDN2 + + if (configurationFileFormat.Version < 20) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); + + configurationFileFormat.ShowConfirmExit = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 21) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); + + // Initialize network config. + + configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 22) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); + + configurationFileFormat.HideCursor = HideCursorMode.Never; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 24) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); + + configurationFileFormat.InputConfig = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + ControllerType = ControllerType.JoyconPair, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 25) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 26) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); + + configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 27) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 28) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 29) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + ShowUI = Key.F4, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 30) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.Rumble = new RumbleConfigController + { + EnableRumble = false, + StrongRumble = 1f, + WeakRumble = 1f, + }; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 31) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); + + configurationFileFormat.BackendThreading = BackendThreading.Auto; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 32) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = Key.F5, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 33) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = Key.F2, + }; + + configurationFileFormat.AudioVolume = 1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 34) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); + + configurationFileFormat.EnableInternetAccess = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 35) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.RangeLeft = 1.0f; + controllerConfig.RangeRight = 1.0f; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 36) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); + + configurationFileFormat.LoggingEnableTrace = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 37) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); + + configurationFileFormat.ShowConsole = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 38) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.BaseStyle = "Dark"; + configurationFileFormat.GameListViewMode = 0; + configurationFileFormat.ShowNames = true; + configurationFileFormat.GridSize = 2; + configurationFileFormat.LanguageCode = "en_US"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 39) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 40) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); + + configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 41) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + } + + if (configurationFileFormat.Version < 42) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); + + configurationFileFormat.EnableMacroHLE = true; + } + + if (configurationFileFormat.Version < 43) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); + + configurationFileFormat.UseHypervisor = true; + } + + if (configurationFileFormat.Version < 44) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); + + configurationFileFormat.AntiAliasing = AntiAliasing.None; + configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; + configurationFileFormat.ScalingFilterLevel = 80; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 45) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); + + configurationFileFormat.ShownFileTypes = new ShownFileTypes + { + NSP = true, + PFS0 = true, + XCI = true, + NCA = true, + NRO = true, + NSO = true, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 46) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); + + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 47) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); + + configurationFileFormat.WindowStartup = new WindowStartup + { + WindowPositionX = 0, + WindowPositionY = 0, + WindowSizeHeight = 760, + WindowSizeWidth = 1280, + WindowMaximized = false, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); + + configurationFileFormat.EnableColorSpacePassthrough = false; + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; + Graphics.ResScale.Value = configurationFileFormat.ResScale; + Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; + Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; + Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; + Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; + Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; + Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; + Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; + Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; + Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; + Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; + Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; + Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; + Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; + Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; + Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; + Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; + Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; + Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; + Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; + System.Language.Value = configurationFileFormat.SystemLanguage; + System.Region.Value = configurationFileFormat.SystemRegion; + System.TimeZone.Value = configurationFileFormat.SystemTimeZone; + System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; + System.EnableDockedMode.Value = configurationFileFormat.DockedMode; + EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; + CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; + ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + HideCursor.Value = configurationFileFormat.HideCursor; + Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; + Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; + Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; + Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; + System.EnablePtc.Value = configurationFileFormat.EnablePtc; + System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; + System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; + System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; + System.AudioBackend.Value = configurationFileFormat.AudioBackend; + System.AudioVolume.Value = configurationFileFormat.AudioVolume; + System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; + System.ExpandRam.Value = configurationFileFormat.ExpandRam; + System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; + System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; + UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; + UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; + UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; + UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; + UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; + UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; + UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; + UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; + UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; + UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; + UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; + UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; + UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; + UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; + UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; + UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; + UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; + UI.EnableCustomTheme.Value = configurationFileFormat.EnableCustomTheme; + UI.LanguageCode.Value = configurationFileFormat.LanguageCode; + UI.CustomThemePath.Value = configurationFileFormat.CustomThemePath; + UI.BaseStyle.Value = configurationFileFormat.BaseStyle; + UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; + UI.ShowNames.Value = configurationFileFormat.ShowNames; + UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; + UI.GridSize.Value = configurationFileFormat.GridSize; + UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; + UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; + UI.ShowConsole.Value = configurationFileFormat.ShowConsole; + UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; + UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; + UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; + UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; + UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; + Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; + Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; + Hid.InputConfig.Value = configurationFileFormat.InputConfig; + + if (Hid.InputConfig.Value == null) + { + Hid.InputConfig.Value = new List(); + } + + Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; + Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + + if (configurationFileUpdated) + { + ToFileFormat().SaveConfig(configurationFilePath); + + Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); + } + } + + private static GraphicsBackend DefaultGraphicsBackend() + { + // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. + // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. + if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) + { + return GraphicsBackend.Vulkan; + } + + return GraphicsBackend.OpenGl; + } + + private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) + { + Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, $"{valueName} set to: {eventArgs.NewValue}"); + } + + public static void Initialize() + { + if (Instance != null) + { + throw new InvalidOperationException("Configuration is already initialized"); + } + + Instance = new ConfigurationState(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/FileTypes.cs b/src/Ryujinx.UI.Common/Configuration/FileTypes.cs new file mode 100644 index 00000000..1974207b --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/FileTypes.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.UI.Common +{ + public enum FileTypes + { + NSP, + PFS0, + XCI, + NCA, + NRO, + NSO + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs new file mode 100644 index 00000000..9cb28359 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs @@ -0,0 +1,113 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Logging.Targets; +using System; +using System.IO; + +namespace Ryujinx.UI.Common.Configuration +{ + public static class LoggerModule + { + public static void Initialize() + { + ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug; + ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub; + ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo; + ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning; + ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError; + ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace; + ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest; + ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog; + ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses; + ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger; + } + + private static void ReloadEnableDebug(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Debug, e.NewValue); + } + + private static void ReloadEnableStub(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Stub, e.NewValue); + } + + private static void ReloadEnableInfo(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Info, e.NewValue); + } + + private static void ReloadEnableWarning(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Warning, e.NewValue); + } + + private static void ReloadEnableError(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Error, e.NewValue); + } + + private static void ReloadEnableTrace(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Trace, e.NewValue); + } + + private static void ReloadEnableGuest(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.Guest, e.NewValue); + } + + private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs e) + { + Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + } + + private static void ReloadFilteredClasses(object sender, ReactiveEventArgs e) + { + bool noFilter = e.NewValue.Length == 0; + + foreach (var logClass in Enum.GetValues()) + { + Logger.SetEnable(logClass, noFilter); + } + + foreach (var logClass in e.NewValue) + { + Logger.SetEnable(logClass, true); + } + } + + private static void ReloadFileLogger(object sender, ReactiveEventArgs e) + { + if (e.NewValue) + { + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + + if (logFile == null) + { + Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + Logger.RemoveTarget("file"); + + return; + } + + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000, + AsyncLogTargetOverflowAction.Block + )); + } + else + { + Logger.RemoveTarget("file"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/System/Language.cs b/src/Ryujinx.UI.Common/Configuration/System/Language.cs new file mode 100644 index 00000000..d1d395b0 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/System/Language.cs @@ -0,0 +1,28 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Configuration.System +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + public enum Language + { + Japanese, + AmericanEnglish, + French, + German, + Italian, + Spanish, + Chinese, + Korean, + Dutch, + Portuguese, + Russian, + Taiwanese, + BritishEnglish, + CanadianFrench, + LatinAmericanSpanish, + SimplifiedChinese, + TraditionalChinese, + BrazilianPortuguese, + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/System/Region.cs b/src/Ryujinx.UI.Common/Configuration/System/Region.cs new file mode 100644 index 00000000..6087c70e --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/System/Region.cs @@ -0,0 +1,17 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Configuration.System +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + public enum Region + { + Japan, + USA, + Europe, + Australia, + China, + Korea, + Taiwan, + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/UI/ColumnSort.cs b/src/Ryujinx.UI.Common/Configuration/UI/ColumnSort.cs new file mode 100644 index 00000000..44e98c40 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/UI/ColumnSort.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.UI.Common.Configuration.UI +{ + public struct ColumnSort + { + public int SortColumnId { get; set; } + public bool SortAscending { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs new file mode 100644 index 00000000..c778ef1f --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.UI.Common.Configuration.UI +{ + public struct GuiColumns + { + public bool FavColumn { get; set; } + public bool IconColumn { get; set; } + public bool AppColumn { get; set; } + public bool DevColumn { get; set; } + public bool VersionColumn { get; set; } + public bool TimePlayedColumn { get; set; } + public bool LastPlayedColumn { get; set; } + public bool FileExtColumn { get; set; } + public bool FileSizeColumn { get; set; } + public bool PathColumn { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/UI/ShownFileTypes.cs b/src/Ryujinx.UI.Common/Configuration/UI/ShownFileTypes.cs new file mode 100644 index 00000000..6c72a693 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/UI/ShownFileTypes.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.UI.Common.Configuration.UI +{ + public struct ShownFileTypes + { + public bool NSP { get; set; } + public bool PFS0 { get; set; } + public bool XCI { get; set; } + public bool NCA { get; set; } + public bool NRO { get; set; } + public bool NSO { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/UI/WindowStartup.cs b/src/Ryujinx.UI.Common/Configuration/UI/WindowStartup.cs new file mode 100644 index 00000000..0df45913 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/UI/WindowStartup.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.UI.Common.Configuration.UI +{ + public struct WindowStartup + { + public int WindowSizeWidth { get; set; } + public int WindowSizeHeight { get; set; } + public int WindowPositionX { get; set; } + public int WindowPositionY { get; set; } + public bool WindowMaximized { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs new file mode 100644 index 00000000..0b9439ea --- /dev/null +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -0,0 +1,98 @@ +using DiscordRPC; +using Ryujinx.Common; +using Ryujinx.UI.Common.Configuration; + +namespace Ryujinx.UI.Common +{ + public static class DiscordIntegrationModule + { + private const string Description = "A simple, experimental Nintendo Switch emulator."; + private const string CliendId = "568815339807309834"; + + private static DiscordRpcClient _discordClient; + private static RichPresence _discordPresenceMain; + + public static void Initialize() + { + _discordPresenceMain = new RichPresence + { + Assets = new Assets + { + LargeImageKey = "ryujinx", + LargeImageText = Description, + }, + Details = "Main Menu", + State = "Idling", + Timestamps = Timestamps.Now, + Buttons = new[] + { + new Button + { + Label = "Website", + Url = "https://ryujinx.org/", + }, + }, + }; + + ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; + } + + private static void Update(object sender, ReactiveEventArgs evnt) + { + if (evnt.OldValue != evnt.NewValue) + { + // If the integration was active, disable it and unload everything + if (evnt.OldValue) + { + _discordClient?.Dispose(); + + _discordClient = null; + } + + // If we need to activate it and the client isn't active, initialize it + if (evnt.NewValue && _discordClient == null) + { + _discordClient = new DiscordRpcClient(CliendId); + + _discordClient.Initialize(); + _discordClient.SetPresence(_discordPresenceMain); + } + } + } + + public static void SwitchToPlayingState(string titleId, string titleName) + { + _discordClient?.SetPresence(new RichPresence + { + Assets = new Assets + { + LargeImageKey = "game", + LargeImageText = titleName, + SmallImageKey = "ryujinx", + SmallImageText = Description, + }, + Details = $"Playing {titleName}", + State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(), + Timestamps = Timestamps.Now, + Buttons = new[] + { + new Button + { + Label = "Website", + Url = "https://ryujinx.org/", + }, + }, + }); + } + + public static void SwitchToMainMenu() + { + _discordClient?.SetPresence(_discordPresenceMain); + } + + public static void Exit() + { + _discordClient?.Dispose(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Extensions/FileTypeExtensions.cs b/src/Ryujinx.UI.Common/Extensions/FileTypeExtensions.cs new file mode 100644 index 00000000..7e71ba7a --- /dev/null +++ b/src/Ryujinx.UI.Common/Extensions/FileTypeExtensions.cs @@ -0,0 +1,25 @@ +using System; +using static Ryujinx.UI.Common.Configuration.ConfigurationState.UISection; + +namespace Ryujinx.UI.Common +{ + public static class FileTypesExtensions + { + /// + /// Gets the current value for the correlating FileType name. + /// + /// The name of the parameter to get the value of. + /// The config instance to get the value from. + /// The current value of the setting. Value is if the file type is the be shown on the games list, otherwise. + public static bool GetConfigValue(this FileTypes type, ShownFileTypeSettings config) => type switch + { + FileTypes.NSP => config.NSP.Value, + FileTypes.PFS0 => config.PFS0.Value, + FileTypes.XCI => config.XCI.Value, + FileTypes.NCA => config.NCA.Value, + FileTypes.NRO => config.NRO.Value, + FileTypes.NSO => config.NSO.Value, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + } +} diff --git a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs new file mode 100644 index 00000000..c3c5bd37 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs @@ -0,0 +1,99 @@ +using Ryujinx.Common.Logging; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Helper +{ + public static class CommandLineState + { + public static string[] Arguments { get; private set; } + + public static bool? OverrideDockedMode { get; private set; } + public static string OverrideGraphicsBackend { get; private set; } + public static string OverrideHideCursor { get; private set; } + public static string BaseDirPathArg { get; private set; } + public static string Profile { get; private set; } + public static string LaunchPathArg { get; private set; } + public static bool StartFullscreenArg { get; private set; } + + public static void ParseArguments(string[] args) + { + List arguments = new(); + + // Parse Arguments. + for (int i = 0; i < args.Length; ++i) + { + string arg = args[i]; + + switch (arg) + { + case "-r": + case "--root-data-dir": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + BaseDirPathArg = args[++i]; + + arguments.Add(arg); + arguments.Add(args[i]); + break; + case "-p": + case "--profile": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + Profile = args[++i]; + + arguments.Add(arg); + arguments.Add(args[i]); + break; + case "-f": + case "--fullscreen": + StartFullscreenArg = true; + + arguments.Add(arg); + break; + case "-g": + case "--graphics-backend": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + OverrideGraphicsBackend = args[++i]; + break; + case "--docked-mode": + OverrideDockedMode = true; + break; + case "--handheld-mode": + OverrideDockedMode = false; + break; + case "--hide-cursor": + if (i + 1 >= args.Length) + { + Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + + continue; + } + + OverrideHideCursor = args[++i]; + break; + default: + LaunchPathArg = arg; + break; + } + } + + Arguments = arguments.ToArray(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs b/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs new file mode 100644 index 00000000..208ff5c9 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs @@ -0,0 +1,50 @@ +using Ryujinx.Common.Logging; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.Helper +{ + public static partial class ConsoleHelper + { + public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows(); + + public static void SetConsoleWindowState(bool show) + { + if (OperatingSystem.IsWindows()) + { + SetConsoleWindowStateWindows(show); + } + else if (show == false) + { + Logger.Warning?.Print(LogClass.Application, "OS doesn't support hiding console window"); + } + } + + [SupportedOSPlatform("windows")] + private static void SetConsoleWindowStateWindows(bool show) + { + const int SW_HIDE = 0; + const int SW_SHOW = 5; + + IntPtr hWnd = GetConsoleWindow(); + + if (hWnd == IntPtr.Zero) + { + Logger.Warning?.Print(LogClass.Application, "Attempted to show/hide console window but console window does not exist"); + return; + } + + ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE); + } + + [SupportedOSPlatform("windows")] + [LibraryImport("kernel32")] + private static partial IntPtr GetConsoleWindow(); + + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); + } +} diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs new file mode 100644 index 00000000..7ed02031 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -0,0 +1,202 @@ +using Microsoft.Win32; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.Helper +{ + public static partial class FileAssociationHelper + { + private static readonly string[] _fileExtensions = { ".nca", ".nro", ".nso", ".nsp", ".xci" }; + + [SupportedOSPlatform("linux")] + private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime"); + + private const int SHCNE_ASSOCCHANGED = 0x8000000; + private const int SHCNF_FLUSH = 0x1000; + + [LibraryImport("shell32.dll", SetLastError = true)] + public static partial void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); + + public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + + [SupportedOSPlatform("linux")] + private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); + + [SupportedOSPlatform("linux")] + private static bool InstallLinuxMimeTypes(bool uninstall = false) + { + string installKeyword = uninstall ? "uninstall" : "install"; + + if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux())) + { + string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml"); + string additionalArgs = !uninstall ? "--novendor" : ""; + + using Process mimeProcess = new(); + + mimeProcess.StartInfo.FileName = "xdg-mime"; + mimeProcess.StartInfo.Arguments = $"{installKeyword} {additionalArgs} --mode user {mimeTypesFile}"; + + mimeProcess.Start(); + mimeProcess.WaitForExit(); + + if (mimeProcess.ExitCode != 0) + { + Logger.Error?.PrintMsg(LogClass.Application, $"Unable to {installKeyword} mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}"); + + return false; + } + + using Process updateMimeProcess = new(); + + updateMimeProcess.StartInfo.FileName = "update-mime-database"; + updateMimeProcess.StartInfo.Arguments = _mimeDbPath; + + updateMimeProcess.Start(); + updateMimeProcess.WaitForExit(); + + if (updateMimeProcess.ExitCode != 0) + { + Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}"); + } + } + + return true; + } + + [SupportedOSPlatform("windows")] + private static bool AreMimeTypesRegisteredWindows() + { + static bool CheckRegistering(string ext) + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); + + if (key is null) + { + return false; + } + + var openCmd = key.OpenSubKey(@"shell\open\command"); + + string keyValue = (string)openCmd.GetValue(""); + + return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); + } + + bool registered = false; + + foreach (string ext in _fileExtensions) + { + registered |= CheckRegistering(ext); + } + + return registered; + } + + [SupportedOSPlatform("windows")] + private static bool InstallWindowsMimeTypes(bool uninstall = false) + { + static bool RegisterExtension(string ext, bool uninstall = false) + { + string keyString = @$"Software\Classes\{ext}"; + + if (uninstall) + { + // If the types don't already exist, there's nothing to do and we can call this operation successful. + if (!AreMimeTypesRegisteredWindows()) + { + return true; + } + Logger.Debug?.Print(LogClass.Application, $"Removing type association {ext}"); + Registry.CurrentUser.DeleteSubKeyTree(keyString); + Logger.Debug?.Print(LogClass.Application, $"Removed type association {ext}"); + } + else + { + using var key = Registry.CurrentUser.CreateSubKey(keyString); + + if (key is null) + { + return false; + } + + Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); + using var openCmd = key.CreateSubKey(@"shell\open\command"); + openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); + Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); + + } + + return true; + } + + bool registered = false; + + foreach (string ext in _fileExtensions) + { + registered |= RegisterExtension(ext, uninstall); + } + + // Notify Explorer the file association has been changed. + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero); + + return registered; + } + + public static bool AreMimeTypesRegistered() + { + if (OperatingSystem.IsLinux()) + { + return AreMimeTypesRegisteredLinux(); + } + + if (OperatingSystem.IsWindows()) + { + return AreMimeTypesRegisteredWindows(); + } + + // TODO: Add macOS support. + + return false; + } + + public static bool Install() + { + if (OperatingSystem.IsLinux()) + { + return InstallLinuxMimeTypes(); + } + + if (OperatingSystem.IsWindows()) + { + return InstallWindowsMimeTypes(); + } + + // TODO: Add macOS support. + + return false; + } + + public static bool Uninstall() + { + if (OperatingSystem.IsLinux()) + { + return InstallLinuxMimeTypes(true); + } + + if (OperatingSystem.IsWindows()) + { + return InstallWindowsMimeTypes(true); + } + + // TODO: Add macOS support. + + return false; + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/LinuxHelper.cs b/src/Ryujinx.UI.Common/Helper/LinuxHelper.cs new file mode 100644 index 00000000..b5779379 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/LinuxHelper.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.Helper +{ + [SupportedOSPlatform("linux")] + public static class LinuxHelper + { + // NOTE: This value was determined by manual tests and might need to be increased again. + public const int RecommendedVmMaxMapCount = 524288; + public const string VmMaxMapCountPath = "/proc/sys/vm/max_map_count"; + public const string SysCtlConfigPath = "/etc/sysctl.d/99-Ryujinx.conf"; + public static int VmMaxMapCount => int.Parse(File.ReadAllText(VmMaxMapCountPath)); + public static string PkExecPath { get; } = GetBinaryPath("pkexec"); + + private static string GetBinaryPath(string binary) + { + string pathVar = Environment.GetEnvironmentVariable("PATH"); + + if (pathVar is null || string.IsNullOrEmpty(binary)) + { + return null; + } + + foreach (var searchPath in pathVar.Split(":", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + string binaryPath = Path.Combine(searchPath, binary); + + if (File.Exists(binaryPath)) + { + return binaryPath; + } + } + + return null; + } + + public static int RunPkExec(string command) + { + if (PkExecPath == null) + { + return 1; + } + + using Process process = new() + { + StartInfo = + { + FileName = PkExecPath, + ArgumentList = { "sh", "-c", command }, + }, + }; + + process.Start(); + process.WaitForExit(); + + return process.ExitCode; + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/ObjectiveC.cs b/src/Ryujinx.UI.Common/Helper/ObjectiveC.cs new file mode 100644 index 00000000..6aba377a --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/ObjectiveC.cs @@ -0,0 +1,160 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.Helper +{ + [SupportedOSPlatform("macos")] + public static partial class ObjectiveC + { + private const string ObjCRuntime = "/usr/lib/libobjc.A.dylib"; + + [LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr sel_getUid(string name); + + [LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr objc_getClass(string name); + + [LibraryImport(ObjCRuntime)] + private static partial void objc_msgSend(IntPtr receiver, Selector selector); + + [LibraryImport(ObjCRuntime)] + private static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value); + + [LibraryImport(ObjCRuntime)] + private static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value); + + [LibraryImport(ObjCRuntime)] + private static partial void objc_msgSend(IntPtr receiver, Selector selector, NSRect point); + + [LibraryImport(ObjCRuntime)] + private static partial void objc_msgSend(IntPtr receiver, Selector selector, double value); + + [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] + private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector); + + [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] + private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param); + + [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend", StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, string param); + + [LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool bool_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param); + + public readonly struct Object + { + public readonly IntPtr ObjPtr; + + private Object(IntPtr pointer) + { + ObjPtr = pointer; + } + + public Object(string name) + { + ObjPtr = objc_getClass(name); + } + + public void SendMessage(Selector selector) + { + objc_msgSend(ObjPtr, selector); + } + + public void SendMessage(Selector selector, byte value) + { + objc_msgSend(ObjPtr, selector, value); + } + + public void SendMessage(Selector selector, Object obj) + { + objc_msgSend(ObjPtr, selector, obj.ObjPtr); + } + + public void SendMessage(Selector selector, NSRect point) + { + objc_msgSend(ObjPtr, selector, point); + } + + public void SendMessage(Selector selector, double value) + { + objc_msgSend(ObjPtr, selector, value); + } + + public Object GetFromMessage(Selector selector) + { + return new Object(IntPtr_objc_msgSend(ObjPtr, selector)); + } + + public Object GetFromMessage(Selector selector, Object obj) + { + return new Object(IntPtr_objc_msgSend(ObjPtr, selector, obj.ObjPtr)); + } + + public Object GetFromMessage(Selector selector, NSString nsString) + { + return new Object(IntPtr_objc_msgSend(ObjPtr, selector, nsString.StrPtr)); + } + + public Object GetFromMessage(Selector selector, string param) + { + return new Object(IntPtr_objc_msgSend(ObjPtr, selector, param)); + } + + public bool GetBoolFromMessage(Selector selector, Object obj) + { + return bool_objc_msgSend(ObjPtr, selector, obj.ObjPtr); + } + } + + public readonly struct Selector + { + public readonly IntPtr SelPtr; + + private Selector(string name) + { + SelPtr = sel_getUid(name); + } + + public static implicit operator Selector(string value) => new(value); + } + + public readonly struct NSString + { + public readonly IntPtr StrPtr; + + public NSString(string aString) + { + IntPtr nsString = objc_getClass("NSString"); + StrPtr = IntPtr_objc_msgSend(nsString, "stringWithUTF8String:", aString); + } + + public static implicit operator IntPtr(NSString nsString) => nsString.StrPtr; + } + + public readonly struct NSPoint + { + public readonly double X; + public readonly double Y; + + public NSPoint(double x, double y) + { + X = x; + Y = y; + } + } + + public readonly struct NSRect + { + public readonly NSPoint Pos; + public readonly NSPoint Size; + + public NSRect(double x, double y, double width, double height) + { + Pos = new NSPoint(x, y); + Size = new NSPoint(width, height); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/OpenHelper.cs b/src/Ryujinx.UI.Common/Helper/OpenHelper.cs new file mode 100644 index 00000000..af6170af --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/OpenHelper.cs @@ -0,0 +1,112 @@ +using Ryujinx.Common.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Ryujinx.UI.Common.Helper +{ + public static partial class OpenHelper + { + [LibraryImport("shell32.dll", SetLastError = true)] + private static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags); + + [LibraryImport("shell32.dll", SetLastError = true)] + private static partial void ILFree(IntPtr pidlList); + + [LibraryImport("shell32.dll", SetLastError = true)] + private static partial IntPtr ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath); + + public static void OpenFolder(string path) + { + if (Directory.Exists(path)) + { + Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true, + Verb = "open", + }); + } + else + { + Logger.Notice.Print(LogClass.Application, $"Directory \"{path}\" doesn't exist!"); + } + } + + public static void LocateFile(string path) + { + if (File.Exists(path)) + { + if (OperatingSystem.IsWindows()) + { + IntPtr pidlList = ILCreateFromPathW(path); + if (pidlList != IntPtr.Zero) + { + try + { + Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0)); + } + finally + { + ILFree(pidlList); + } + } + } + else if (OperatingSystem.IsMacOS()) + { + ObjectiveC.NSString nsStringPath = new(path); + ObjectiveC.Object nsUrl = new("NSURL"); + var urlPtr = nsUrl.GetFromMessage("fileURLWithPath:", nsStringPath); + + ObjectiveC.Object nsArray = new("NSArray"); + ObjectiveC.Object urlArray = nsArray.GetFromMessage("arrayWithObject:", urlPtr); + + ObjectiveC.Object nsWorkspace = new("NSWorkspace"); + ObjectiveC.Object sharedWorkspace = nsWorkspace.GetFromMessage("sharedWorkspace"); + + sharedWorkspace.SendMessage("activateFileViewerSelectingURLs:", urlArray); + } + else if (OperatingSystem.IsLinux()) + { + Process.Start("dbus-send", $"--session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\""); + } + else + { + OpenFolder(Path.GetDirectoryName(path)); + } + } + else + { + Logger.Notice.Print(LogClass.Application, $"File \"{path}\" doesn't exist!"); + } + } + + public static void OpenUrl(string url) + { + if (OperatingSystem.IsWindows()) + { + Process.Start(new ProcessStartInfo("cmd", $"/c start {url.Replace("&", "^&")}")); + } + else if (OperatingSystem.IsLinux()) + { + Process.Start("xdg-open", url); + } + else if (OperatingSystem.IsMacOS()) + { + ObjectiveC.NSString nsStringPath = new(url); + ObjectiveC.Object nsUrl = new("NSURL"); + var urlPtr = nsUrl.GetFromMessage("URLWithString:", nsStringPath); + + ObjectiveC.Object nsWorkspace = new("NSWorkspace"); + ObjectiveC.Object sharedWorkspace = nsWorkspace.GetFromMessage("sharedWorkspace"); + + sharedWorkspace.GetBoolFromMessage("openURL:", urlPtr); + } + else + { + Logger.Notice.Print(LogClass.Application, $"Cannot open url \"{url}\" on this platform!"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/SetupValidator.cs b/src/Ryujinx.UI.Common/Helper/SetupValidator.cs new file mode 100644 index 00000000..a954be26 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/SetupValidator.cs @@ -0,0 +1,114 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using System; +using System.IO; + +namespace Ryujinx.UI.Common.Helper +{ + /// + /// Ensure installation validity + /// + public static class SetupValidator + { + public static bool IsFirmwareValid(ContentManager contentManager, out UserError error) + { + bool hasFirmware = contentManager.GetCurrentFirmwareVersion() != null; + + if (hasFirmware) + { + error = UserError.Success; + + return true; + } + + error = UserError.NoFirmware; + + return false; + } + + public static bool CanFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out SystemVersion firmwareVersion) + { + try + { + firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); + } + catch (Exception) + { + firmwareVersion = null; + } + + return error == UserError.NoFirmware && Path.GetExtension(baseApplicationPath).ToLowerInvariant() == ".xci" && firmwareVersion != null; + } + + public static bool TryFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out UserError outError) + { + if (error == UserError.NoFirmware) + { + string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); + + // If the target app to start is a XCI, try to install firmware from it + if (baseApplicationExtension == ".xci") + { + SystemVersion firmwareVersion; + + try + { + firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); + } + catch (Exception) + { + firmwareVersion = null; + } + + // The XCI is a valid firmware package, try to install the firmware from it! + if (firmwareVersion != null) + { + try + { + Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + + contentManager.InstallFirmware(baseApplicationPath); + + Logger.Info?.Print(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed."); + + outError = UserError.Success; + + return true; + } + catch (Exception) { } + } + + outError = error; + + return false; + } + } + + outError = error; + + return false; + } + + public static bool CanStartApplication(ContentManager contentManager, string baseApplicationPath, out UserError error) + { + if (Directory.Exists(baseApplicationPath) || File.Exists(baseApplicationPath)) + { + string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); + + // NOTE: We don't force homebrew developers to install a system firmware. + if (baseApplicationExtension == ".nro" || baseApplicationExtension == ".nso") + { + error = UserError.Success; + + return true; + } + + return IsFirmwareValid(contentManager, out error); + } + + error = UserError.ApplicationNotFound; + + return false; + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs new file mode 100644 index 00000000..c2085b28 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs @@ -0,0 +1,162 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using ShellLink; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.Helper +{ + public static class ShortcutHelper + { + [SupportedOSPlatform("windows")] + private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe"); + iconPath += ".ico"; + + MemoryStream iconDataStream = new(iconData); + var image = Image.Load(iconDataStream); + image.Mutate(x => x.Resize(128, 128)); + SaveBitmapAsIcon(image, iconPath); + + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0); + shortcut.StringData.NameString = cleanedAppName; + shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); + } + + [SupportedOSPlatform("linux")] + private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh"); + var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop"); + iconPath += ".png"; + + var image = Image.Load(iconData); + image.SaveAsPng(iconPath); + + using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); + outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}"); + } + + [SupportedOSPlatform("macos")] + private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"); + var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist"); + var shortcutScript = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-launch-script.sh"); + // Macos .App folder + string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents"); + string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); + + if (!Directory.Exists(scriptFolderPath)) + { + Directory.CreateDirectory(scriptFolderPath); + } + + // Runner script + const string ScriptName = "runner.sh"; + string scriptPath = Path.Combine(scriptFolderPath, ScriptName); + using StreamWriter scriptFile = new(scriptPath); + + scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath)); + + // Set execute permission + FileInfo fileInfo = new(scriptPath); + fileInfo.UnixFileMode |= UnixFileMode.UserExecute; + + // img + string resourceFolderPath = Path.Combine(contentFolderPath, "Resources"); + if (!Directory.Exists(resourceFolderPath)) + { + Directory.CreateDirectory(resourceFolderPath); + } + + const string IconName = "icon.png"; + var image = Image.Load(iconData); + image.SaveAsPng(Path.Combine(resourceFolderPath, IconName)); + + // plist file + using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist")); + outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName); + } + + public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData) + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars())); + + if (OperatingSystem.IsWindows()) + { + string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app"); + + CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath); + + return; + } + + if (OperatingSystem.IsLinux()) + { + string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx"); + + Directory.CreateDirectory(iconPath); + CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName); + + return; + } + + if (OperatingSystem.IsMacOS()) + { + CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName); + + return; + } + + throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); + } + + private static string GetArgsString(string appFilePath) + { + // args are first defined as a list, for easier adjustments in the future + var argsList = new List(); + + if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) + { + argsList.Add("--root-data-dir"); + argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); + } + + argsList.Add($"\"{appFilePath}\""); + + return String.Join(" ", argsList); + } + + /// + /// Creates a Icon (.ico) file using the source bitmap image at the specified file path. + /// + /// The source bitmap image that will be saved as an .ico file + /// The location that the new .ico file will be saved too (Make sure to include '.ico' in the path). + [SupportedOSPlatform("windows")] + private static void SaveBitmapAsIcon(Image source, string filePath) + { + // Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz + byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 }; + using FileStream fs = new(filePath, FileMode.Create); + + fs.Write(header); + // Writing actual data + source.Save(fs, PngFormat.Instance); + // Getting data length (file length minus header) + long dataLength = fs.Length - header.Length; + // Write it in the correct place + fs.Seek(14, SeekOrigin.Begin); + fs.WriteByte((byte)dataLength); + fs.WriteByte((byte)(dataLength >> 8)); + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/TitleHelper.cs b/src/Ryujinx.UI.Common/Helper/TitleHelper.cs new file mode 100644 index 00000000..8b47ac38 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/TitleHelper.cs @@ -0,0 +1,30 @@ +using Ryujinx.HLE.Loaders.Processes; +using System; + +namespace Ryujinx.UI.Common.Helper +{ + public static class TitleHelper + { + public static string ActiveApplicationTitle(ProcessResult activeProcess, string applicationVersion, string pauseString = "") + { + if (activeProcess == null) + { + return String.Empty; + } + + string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}"; + string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}"; + string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})"; + string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)"; + + string appTitle = $"Ryujinx {applicationVersion} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; + + if (!string.IsNullOrEmpty(pauseString)) + { + appTitle += $" ({pauseString})"; + } + + return appTitle; + } + } +} diff --git a/src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs new file mode 100644 index 00000000..8ea3e721 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs @@ -0,0 +1,219 @@ +using System; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.UI.Common.Helper +{ + public static class ValueFormatUtils + { + private static readonly string[] _fileSizeUnitStrings = + { + "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing + "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values + }; + + /// + /// Used by . + /// + public enum FileSizeUnits + { + Auto = -1, + Bytes = 0, + Kibibytes = 1, + Mebibytes = 2, + Gibibytes = 3, + Tebibytes = 4, + Pebibytes = 5, + Exbibytes = 6, + Kilobytes = 7, + Megabytes = 8, + Gigabytes = 9, + Terabytes = 10, + Petabytes = 11, + Exabytes = 12, + } + + private const double SizeBase10 = 1000; + private const double SizeBase2 = 1024; + private const int UnitEBIndex = 6; + + #region Value formatters + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. + /// A formatted string that can be displayed in the UI. + public static string FormatTimeSpan(TimeSpan? timeSpan) + { + if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1) + { + // Game was never played + return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture); + } + + if (timeSpan.Value.TotalDays < 1) + { + // Game was played for less than a day + return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture); + } + + // Game was played for more than a day + TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days)); + string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture); + + return $"{timeSpan.Value.Days}d, {onlyTimeString}"; + } + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. This is expected to be UTC-based. + /// The that's used in formatting. Defaults to . + /// A formatted string that can be displayed in the UI. + public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) + { + culture ??= CultureInfo.CurrentCulture; + + if (!utcDateTime.HasValue) + { + // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter. + return "Never"; + } + + return utcDateTime.Value.ToLocalTime().ToString(culture); + } + + /// + /// Creates a human-readable file size string. + /// + /// The file size in bytes. + /// Formats the passed size value as this unit, bypassing the automatic unit choice. + /// A human-readable file size string. + public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto) + { + if (size <= 0) + { + return $"0 {_fileSizeUnitStrings[0]}"; + } + + int unitIndex = (int)forceUnit; + if (forceUnit == FileSizeUnits.Auto) + { + unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10))); + + // Apply an upper bound so that exabytes are the biggest unit used when formatting. + if (unitIndex > UnitEBIndex) + { + unitIndex = UnitEBIndex; + } + } + + double sizeRounded; + + if (unitIndex > UnitEBIndex) + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1); + } + else + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1); + } + + string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture); + + return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}"; + } + + #endregion + + #region Value parsers + + /// + /// Parses a string generated by and returns the original . + /// + /// A string representing a . + /// A object. If the input string couldn't been parsed, is returned. + public static TimeSpan ParseTimeSpan(string timeSpanString) + { + TimeSpan returnTimeSpan = TimeSpan.Zero; + + // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day. + // Here, we split the input string to check if it's the former or the latter. + var valueSplit = timeSpanString.Split(", "); + if (valueSplit.Length > 1) + { + var dayPart = valueSplit[0].Split("d")[0]; + if (int.TryParse(dayPart, out int days)) + { + returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days)); + } + } + + if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan)) + { + returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan); + } + + return returnTimeSpan; + } + + /// + /// Parses a string generated by and returns the original . + /// + /// The string representing a . + /// A object. If the input string couldn't be parsed, is returned. + public static DateTime ParseDateTime(string dateTimeString) + { + if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime)) + { + // Games that were never played are supposed to appear before the oldest played games in the list, + // so returning DateTime.UnixEpoch here makes sense. + return DateTime.UnixEpoch; + } + + return parsedDateTime; + } + + /// + /// Parses a string generated by and returns a representing a number of bytes. + /// + /// A string representing a file size formatted with . + /// A representing a number of bytes. + public static long ParseFileSize(string sizeString) + { + // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration. + for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--) + { + string unit = _fileSizeUnitStrings[i]; + if (!sizeString.EndsWith(unit)) + { + continue; + } + + string numberString = sizeString.Split(" ")[0]; + if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number)) + { + break; + } + + double sizeBase = SizeBase2; + + // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value. + if (i > UnitEBIndex) + { + i -= UnitEBIndex; + sizeBase = SizeBase10; + } + + number *= Math.Pow(sizeBase, i); + + return Convert.ToInt64(number); + } + + return 0; + } + + #endregion + } +} diff --git a/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs new file mode 100644 index 00000000..7989f0f1 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Amiibo +{ + public struct AmiiboApi : IEquatable + { + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("head")] + public string Head { get; set; } + [JsonPropertyName("tail")] + public string Tail { get; set; } + [JsonPropertyName("image")] + public string Image { get; set; } + [JsonPropertyName("amiiboSeries")] + public string AmiiboSeries { get; set; } + [JsonPropertyName("character")] + public string Character { get; set; } + [JsonPropertyName("gameSeries")] + public string GameSeries { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("release")] + public Dictionary Release { get; set; } + + [JsonPropertyName("gamesSwitch")] + public List GamesSwitch { get; set; } + + public readonly override string ToString() + { + return Name; + } + + public readonly string GetId() + { + return Head + Tail; + } + + public readonly bool Equals(AmiiboApi other) + { + return Head + Tail == other.Head + other.Tail; + } + + public readonly override bool Equals(object obj) + { + return obj is AmiiboApi other && Equals(other); + } + + public readonly override int GetHashCode() + { + return HashCode.Combine(Head, Tail); + } + + public static bool operator ==(AmiiboApi left, AmiiboApi right) + { + return left.Equals(right); + } + + public static bool operator !=(AmiiboApi left, AmiiboApi right) + { + return !(left == right); + } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs new file mode 100644 index 00000000..40e635bf --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Amiibo +{ + public class AmiiboApiGamesSwitch + { + [JsonPropertyName("amiiboUsage")] + public List AmiiboUsage { get; set; } + [JsonPropertyName("gameID")] + public List GameId { get; set; } + [JsonPropertyName("gameName")] + public string GameName { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiUsage.cs b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiUsage.cs new file mode 100644 index 00000000..4f8d292b --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiUsage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Amiibo +{ + public class AmiiboApiUsage + { + [JsonPropertyName("Usage")] + public string Usage { get; set; } + [JsonPropertyName("write")] + public bool Write { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs new file mode 100644 index 00000000..15083f50 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Amiibo +{ + public struct AmiiboJson + { + [JsonPropertyName("amiibo")] + public List Amiibo { get; set; } + [JsonPropertyName("lastUpdated")] + public DateTime LastUpdated { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs new file mode 100644 index 00000000..bc3f1303 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Amiibo +{ + [JsonSerializable(typeof(AmiiboJson))] + public partial class AmiiboJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/Ryujinx.UI.Common/Models/Github/GithubReleaseAssetJsonResponse.cs b/src/Ryujinx.UI.Common/Models/Github/GithubReleaseAssetJsonResponse.cs new file mode 100644 index 00000000..8f528dc0 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Github/GithubReleaseAssetJsonResponse.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.UI.Common.Models.Github +{ + public class GithubReleaseAssetJsonResponse + { + public string Name { get; set; } + public string State { get; set; } + public string BrowserDownloadUrl { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs new file mode 100644 index 00000000..0250e109 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Models.Github +{ + public class GithubReleasesJsonResponse + { + public string Name { get; set; } + public List Assets { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonSerializerContext.cs b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonSerializerContext.cs new file mode 100644 index 00000000..71864257 --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.Common.Models.Github +{ + [JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)] + public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg b/src/Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg new file mode 100644 index 00000000..03585e65 --- /dev/null +++ b/src/Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg b/src/Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg new file mode 100644 index 00000000..c073c9c0 --- /dev/null +++ b/src/Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg b/src/Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg new file mode 100644 index 00000000..f4f12514 --- /dev/null +++ b/src/Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Ryujinx.UI.Common/Resources/Controller_ProCon.svg b/src/Ryujinx.UI.Common/Resources/Controller_ProCon.svg new file mode 100644 index 00000000..f5380f3a --- /dev/null +++ b/src/Ryujinx.UI.Common/Resources/Controller_ProCon.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + + + + X + + + + Y + + + + B + + + + + + + + + + ZL + + + + + ZR + + + + R + + + + + L + + + diff --git a/src/Ryujinx.UI.Common/Resources/Icon_NCA.png b/src/Ryujinx.UI.Common/Resources/Icon_NCA.png new file mode 100644 index 00000000..feae77b9 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_NCA.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Icon_NRO.png b/src/Ryujinx.UI.Common/Resources/Icon_NRO.png new file mode 100644 index 00000000..3a9da621 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_NRO.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Icon_NSO.png b/src/Ryujinx.UI.Common/Resources/Icon_NSO.png new file mode 100644 index 00000000..16de84be Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_NSO.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Icon_NSP.png b/src/Ryujinx.UI.Common/Resources/Icon_NSP.png new file mode 100644 index 00000000..4f98e22e Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_NSP.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Icon_XCI.png b/src/Ryujinx.UI.Common/Resources/Icon_XCI.png new file mode 100644 index 00000000..f9c34f47 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Icon_XCI.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png b/src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png new file mode 100644 index 00000000..cbee8037 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Discord_Dark.png b/src/Ryujinx.UI.Common/Resources/Logo_Discord_Dark.png new file mode 100644 index 00000000..baececa9 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Discord_Dark.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Discord_Light.png b/src/Ryujinx.UI.Common/Resources/Logo_Discord_Light.png new file mode 100644 index 00000000..25fc892d Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Discord_Light.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Dark.png b/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Dark.png new file mode 100644 index 00000000..50b81752 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Dark.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Light.png b/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Light.png new file mode 100644 index 00000000..95bc742b Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_GitHub_Light.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Dark.png b/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Dark.png new file mode 100644 index 00000000..9a521e3f Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Dark.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Light.png b/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Light.png new file mode 100644 index 00000000..44da0ac4 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Patreon_Light.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png b/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png new file mode 100644 index 00000000..0e8da15e Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Dark.png b/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Dark.png new file mode 100644 index 00000000..66962e7d Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Dark.png differ diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Light.png b/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Light.png new file mode 100644 index 00000000..040ca169 Binary files /dev/null and b/src/Ryujinx.UI.Common/Resources/Logo_Twitter_Light.png differ diff --git a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj new file mode 100644 index 00000000..387e998b --- /dev/null +++ b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj @@ -0,0 +1,68 @@ + + + + net8.0 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.UI.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.UI.Common/SystemInfo/LinuxSystemInfo.cs new file mode 100644 index 00000000..c7fe05a0 --- /dev/null +++ b/src/Ryujinx.UI.Common/SystemInfo/LinuxSystemInfo.cs @@ -0,0 +1,85 @@ +using Ryujinx.Common.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.SystemInfo +{ + [SupportedOSPlatform("linux")] + class LinuxSystemInfo : SystemInfo + { + internal LinuxSystemInfo() + { + string cpuName = GetCpuidCpuName(); + + if (cpuName == null) + { + var cpuDict = new Dictionary(StringComparer.Ordinal) + { + ["model name"] = null, + ["Processor"] = null, + ["Hardware"] = null, + }; + + ParseKeyValues("/proc/cpuinfo", cpuDict); + + cpuName = cpuDict["model name"] ?? cpuDict["Processor"] ?? cpuDict["Hardware"] ?? "Unknown"; + } + + var memDict = new Dictionary(StringComparer.Ordinal) + { + ["MemTotal"] = null, + ["MemAvailable"] = null, + }; + + ParseKeyValues("/proc/meminfo", memDict); + + // Entries are in KiB + ulong.TryParse(memDict["MemTotal"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong totalKiB); + ulong.TryParse(memDict["MemAvailable"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong availableKiB); + + CpuName = $"{cpuName} ; {LogicalCoreCount} logical"; + RamTotal = totalKiB * 1024; + RamAvailable = availableKiB * 1024; + } + + private static void ParseKeyValues(string filePath, Dictionary itemDict) + { + if (!File.Exists(filePath)) + { + Logger.Error?.Print(LogClass.Application, $"File \"{filePath}\" not found"); + + return; + } + + int count = itemDict.Count; + + using StreamReader file = new(filePath); + + string line; + while ((line = file.ReadLine()) != null) + { + string[] kvPair = line.Split(':', 2, StringSplitOptions.TrimEntries); + + if (kvPair.Length < 2) + { + continue; + } + + string key = kvPair[0]; + + if (itemDict.TryGetValue(key, out string value) && value == null) + { + itemDict[key] = kvPair[1]; + + if (--count <= 0) + { + break; + } + } + } + } + } +} diff --git a/src/Ryujinx.UI.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.UI.Common/SystemInfo/MacOSSystemInfo.cs new file mode 100644 index 00000000..36deaf35 --- /dev/null +++ b/src/Ryujinx.UI.Common/SystemInfo/MacOSSystemInfo.cs @@ -0,0 +1,164 @@ +using Ryujinx.Common.Logging; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; + +namespace Ryujinx.UI.Common.SystemInfo +{ + [SupportedOSPlatform("macos")] + partial class MacOSSystemInfo : SystemInfo + { + internal MacOSSystemInfo() + { + if (SysctlByName("kern.osversion", out string buildRevision) != 0) + { + buildRevision = "Unknown Build"; + } + + OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})"; + + string cpuName = GetCpuidCpuName(); + + if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0) + { + cpuName = "Unknown"; + } + + ulong totalRAM = 0; + + if (SysctlByName("hw.memsize", ref totalRAM) != 0) // Bytes + { + totalRAM = 0; + } + + CpuName = $"{cpuName} ; {LogicalCoreCount} logical"; + RamTotal = totalRAM; + RamAvailable = GetVMInfoAvailableMemory(); + } + + static ulong GetVMInfoAvailableMemory() + { + var port = mach_host_self(); + + uint pageSize = 0; + var result = host_page_size(port, ref pageSize); + + if (result != 0) + { + Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_page_size() error = {result}"); + return 0; + } + + const int Flavor = 4; // HOST_VM_INFO64 + uint count = (uint)(Marshal.SizeOf() / sizeof(int)); // HOST_VM_INFO64_COUNT + VMStatistics64 stats = new(); + result = host_statistics64(port, Flavor, ref stats, ref count); + + if (result != 0) + { + Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_statistics64() error = {result}"); + return 0; + } + + return (ulong)(stats.FreeCount + stats.InactiveCount) * pageSize; + } + + private const string SystemLibraryName = "libSystem.dylib"; + + [LibraryImport(SystemLibraryName, SetLastError = true)] + private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, IntPtr oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize); + + private static int SysctlByName(string name, IntPtr oldValue, ref ulong oldSize) + { + if (sysctlbyname(name, oldValue, ref oldSize, IntPtr.Zero, 0) == -1) + { + int err = Marshal.GetLastWin32Error(); + + Logger.Error?.Print(LogClass.Application, $"Cannot retrieve '{name}'. Error Code {err}"); + + return err; + } + + return 0; + } + + private static int SysctlByName(string name, ref T oldValue) + { + unsafe + { + ulong oldValueSize = (ulong)Unsafe.SizeOf(); + + return SysctlByName(name, (IntPtr)Unsafe.AsPointer(ref oldValue), ref oldValueSize); + } + } + + private static int SysctlByName(string name, out string oldValue) + { + oldValue = default; + + ulong strSize = 0; + + int res = SysctlByName(name, IntPtr.Zero, ref strSize); + + if (res == 0) + { + byte[] rawData = new byte[strSize]; + + unsafe + { + fixed (byte* rawDataPtr = rawData) + { + res = SysctlByName(name, (IntPtr)rawDataPtr, ref strSize); + } + + if (res == 0) + { + oldValue = Encoding.ASCII.GetString(rawData); + } + } + } + + return res; + } + + [LibraryImport(SystemLibraryName, SetLastError = true)] + private static partial uint mach_host_self(); + + [LibraryImport(SystemLibraryName, SetLastError = true)] + private static partial int host_page_size(uint host, ref uint out_page_size); + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + struct VMStatistics64 + { + public uint FreeCount; + public uint ActiveCount; + public uint InactiveCount; + public uint WireCount; + public ulong ZeroFillCount; + public ulong Reactivations; + public ulong Pageins; + public ulong Pageouts; + public ulong Faults; + public ulong CowFaults; + public ulong Lookups; + public ulong Hits; + public ulong Purges; + public uint PurgeableCount; + public uint SpeculativeCount; + public ulong Decompressions; + public ulong Compressions; + public ulong Swapins; + public ulong Swapouts; + public uint CompressorPageCount; + public uint ThrottledCount; + public uint ExternalPageCount; + public uint InternalPageCount; + public ulong TotalUncompressedPagesInCompressor; + } + + [LibraryImport(SystemLibraryName, SetLastError = true)] + private static partial int host_statistics64(uint hostPriv, int hostFlavor, ref VMStatistics64 hostInfo64Out, ref uint hostInfo64OutCnt); + } +} diff --git a/src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs new file mode 100644 index 00000000..38728b9c --- /dev/null +++ b/src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs @@ -0,0 +1,79 @@ +using Ryujinx.Common.Logging; +using Ryujinx.UI.Common.Helper; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; +using System.Text; + +namespace Ryujinx.UI.Common.SystemInfo +{ + public class SystemInfo + { + public string OsDescription { get; protected set; } + public string CpuName { get; protected set; } + public ulong RamTotal { get; protected set; } + public ulong RamAvailable { get; protected set; } + protected static int LogicalCoreCount => Environment.ProcessorCount; + + protected SystemInfo() + { + OsDescription = $"{RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"; + CpuName = "Unknown"; + } + + private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes); + + public void Print() + { + Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}"); + Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}"); + Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}"); + } + + public static SystemInfo Gather() + { + if (OperatingSystem.IsWindows()) + { + return new WindowsSystemInfo(); + } + else if (OperatingSystem.IsLinux()) + { + return new LinuxSystemInfo(); + } + else if (OperatingSystem.IsMacOS()) + { + return new MacOSSystemInfo(); + } + + Logger.Error?.Print(LogClass.Application, "SystemInfo unsupported on this platform"); + + return new SystemInfo(); + } + + // x86 exposes a 48 byte ASCII "CPU brand" string via CPUID leaves 0x80000002-0x80000004. + internal static string GetCpuidCpuName() + { + if (!X86Base.IsSupported) + { + return null; + } + + // Check if CPU supports the query + if ((uint)X86Base.CpuId(unchecked((int)0x80000000), 0).Eax < 0x80000004) + { + return null; + } + + int[] regs = new int[12]; + + for (uint i = 0; i < 3; ++i) + { + (regs[4 * i], regs[4 * i + 1], regs[4 * i + 2], regs[4 * i + 3]) = X86Base.CpuId((int)(0x80000002 + i), 0); + } + + string name = Encoding.ASCII.GetString(MemoryMarshal.Cast(regs)).Replace('\0', ' ').Trim(); + + return string.IsNullOrEmpty(name) ? null : name; + } + } +} diff --git a/src/Ryujinx.UI.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.UI.Common/SystemInfo/WindowsSystemInfo.cs new file mode 100644 index 00000000..bf49c2a6 --- /dev/null +++ b/src/Ryujinx.UI.Common/SystemInfo/WindowsSystemInfo.cs @@ -0,0 +1,87 @@ +using Ryujinx.Common.Logging; +using System; +using System.Management; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.UI.Common.SystemInfo +{ + [SupportedOSPlatform("windows")] + partial class WindowsSystemInfo : SystemInfo + { + internal WindowsSystemInfo() + { + CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow + (RamTotal, RamAvailable) = GetMemoryStats(); + } + + private static (ulong Total, ulong Available) GetMemoryStats() + { + MemoryStatusEx memStatus = new(); + if (GlobalMemoryStatusEx(ref memStatus)) + { + return (memStatus.TotalPhys, memStatus.AvailPhys); // Bytes + } + + Logger.Error?.Print(LogClass.Application, $"GlobalMemoryStatusEx failed. Error {Marshal.GetLastWin32Error():X}"); + + return (0, 0); + } + + private static string GetCpuNameWMI() + { + ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor"); + + if (cpuObjs != null) + { + foreach (var cpuObj in cpuObjs) + { + return cpuObj["Name"].ToString().Trim(); + } + } + + return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER").Trim(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct MemoryStatusEx + { + public uint Length; + public uint MemoryLoad; + public ulong TotalPhys; + public ulong AvailPhys; + public ulong TotalPageFile; + public ulong AvailPageFile; + public ulong TotalVirtual; + public ulong AvailVirtual; + public ulong AvailExtendedVirtual; + + public MemoryStatusEx() + { + Length = (uint)Marshal.SizeOf(); + } + } + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer); + + private static ManagementObjectCollection GetWMIObjects(string scope, string query) + { + try + { + return new ManagementObjectSearcher(scope, query).Get(); + } + catch (PlatformNotSupportedException ex) + { + Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}"); + } + catch (COMException ex) + { + Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}"); + } + + return null; + } + } +} diff --git a/src/Ryujinx.UI.Common/UserError.cs b/src/Ryujinx.UI.Common/UserError.cs new file mode 100644 index 00000000..706971ef --- /dev/null +++ b/src/Ryujinx.UI.Common/UserError.cs @@ -0,0 +1,39 @@ +namespace Ryujinx.UI.Common +{ + /// + /// Represent a common error that could be reported to the user by the emulator. + /// + public enum UserError + { + /// + /// No error to report. + /// + Success = 0x0, + + /// + /// No keys are present. + /// + NoKeys = 0x1, + + /// + /// No firmware is installed. + /// + NoFirmware = 0x2, + + /// + /// Firmware parsing failed. + /// + /// Most likely related to keys. + FirmwareParsingFailed = 0x3, + + /// + /// No application was found at the given path. + /// + ApplicationNotFound = 0x4, + + /// + /// An unknown error. + /// + Unknown = 0xDEAD, + } +} -- cgit v1.2.3