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
9
src/Ryujinx.UI.Common/App/ApplicationAddedEventArgs.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.UI.App.Common
|
||||
{
|
||||
public class ApplicationAddedEventArgs : EventArgs
|
||||
{
|
||||
public ApplicationData AppData { get; set; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
158
src/Ryujinx.UI.Common/App/ApplicationData.cs
Normal file
|
@ -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<ApplicationControlProperty> 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<IFile>();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new(virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||
|
||||
if (nca.Header.ContentType != NcaContentType.Program)
|
||||
{
|
||||
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<IFile>();
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
930
src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
Normal file
|
@ -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<ApplicationAddedEventArgs> ApplicationAdded;
|
||||
public event EventHandler<ApplicationCountUpdatedEventArgs> 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<byte> outProperty)
|
||||
{
|
||||
using UniqueRef<IFile> 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<string> appDirs, Language desiredTitleLanguage)
|
||||
{
|
||||
int numApplicationsFound = 0;
|
||||
int numApplicationsLoaded = 0;
|
||||
|
||||
_desiredTitleLanguage = desiredTitleLanguage;
|
||||
|
||||
_cancellationToken = new CancellationTokenSource();
|
||||
|
||||
// Builds the applications list with paths to found applications
|
||||
List<string> 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<string> 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<ApplicationControlProperty> 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<IFile> 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<IFile> 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<IFile> 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<IFile>();
|
||||
|
||||
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<ApplicationMetadata> 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<IFile>();
|
||||
|
||||
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<IFile>();
|
||||
|
||||
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<IFile>();
|
||||
|
||||
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<IFile>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
51
src/Ryujinx.UI.Common/App/ApplicationMetadata.cs
Normal file
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
|
||||
/// </summary>
|
||||
public void UpdatePreGame()
|
||||
{
|
||||
LastPlayed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
14
src/Ryujinx.UI.Common/Configuration/AudioBackend.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.UI.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))]
|
||||
public enum AudioBackend
|
||||
{
|
||||
Dummy,
|
||||
OpenAl,
|
||||
SoundIo,
|
||||
SDL2,
|
||||
}
|
||||
}
|
409
src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
Normal file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 48;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
/// </summary>
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables logging to a file on disk
|
||||
/// </summary>
|
||||
public bool EnableFileLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime.
|
||||
/// </summary>
|
||||
public BackendThreading BackendThreading { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
|
||||
/// </summary>
|
||||
public int ResScale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
|
||||
/// </summary>
|
||||
public float ResScaleCustom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
|
||||
/// </summary>
|
||||
public float MaxAnisotropy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Aspect Ratio applied to the renderer window.
|
||||
/// </summary>
|
||||
public AspectRatio AspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies anti-aliasing to the renderer.
|
||||
/// </summary>
|
||||
public AntiAliasing AntiAliasing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling type.
|
||||
/// </summary>
|
||||
public ScalingFilter ScalingFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling level.
|
||||
/// </summary>
|
||||
public int ScalingFilterLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// </summary>
|
||||
public string GraphicsShadersDumpPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing debug log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableDebug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing stub log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableStub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing info log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing warning log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableWarn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing error log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing trace log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableTrace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing guest log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableGuest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing FS access log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableFsAccessLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which log messages are written to the log targets
|
||||
/// </summary>
|
||||
public LogClass[] LoggingFilteredClasses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change Graphics API debug log level
|
||||
/// </summary>
|
||||
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Language
|
||||
/// </summary>
|
||||
public Language SystemLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Region
|
||||
/// </summary>
|
||||
public Region SystemRegion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System TimeZone
|
||||
/// </summary>
|
||||
public string SystemTimeZone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Time Offset in seconds
|
||||
/// </summary>
|
||||
public long SystemTimeOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Docked Mode
|
||||
/// </summary>
|
||||
public bool DockedMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Discord Rich Presence
|
||||
/// </summary>
|
||||
public bool EnableDiscordIntegration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when Ryujinx starts when enabled
|
||||
/// </summary>
|
||||
public bool CheckUpdatesOnStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show "Confirm Exit" Dialog
|
||||
/// </summary>
|
||||
public bool ShowConfirmExit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to hide cursor on idle, always or never
|
||||
/// </summary>
|
||||
public HideCursorMode HideCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
public bool EnableVsync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Shader cache
|
||||
/// </summary>
|
||||
public bool EnableShaderCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables texture recompression
|
||||
/// </summary>
|
||||
public bool EnableTextureRecompression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Macro high-level emulation
|
||||
/// </summary>
|
||||
public bool EnableMacroHLE { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables color space passthrough, if available.
|
||||
/// </summary>
|
||||
public bool EnableColorSpacePassthrough { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables profiled translation cache persistency
|
||||
/// </summary>
|
||||
public bool EnablePtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables guest Internet access
|
||||
/// </summary>
|
||||
public bool EnableInternetAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables integrity checks on Game content files
|
||||
/// </summary>
|
||||
public bool EnableFsIntegrityChecks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables FS access log output to the console. Possible modes are 0-3
|
||||
/// </summary>
|
||||
public int FsGlobalAccessLogMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected audio backend
|
||||
/// </summary>
|
||||
public AudioBackend AudioBackend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The audio volume
|
||||
/// </summary>
|
||||
public float AudioVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected memory manager mode
|
||||
/// </summary>
|
||||
public MemoryManagerMode MemoryManagerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expands the RAM amount on the emulated system from 4GiB to 6GiB
|
||||
/// </summary>
|
||||
public bool ExpandRam { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable ignoring missing services
|
||||
/// </summary>
|
||||
public bool IgnoreMissingServices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle columns in the GUI
|
||||
/// </summary>
|
||||
public GuiColumns GuiColumns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure column sort settings in the GUI
|
||||
/// </summary>
|
||||
public ColumnSort ColumnSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing games to be used to load games into the games list
|
||||
/// </summary>
|
||||
public List<string> GameDirs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of file types to be hidden in the games List
|
||||
/// </summary>
|
||||
public ShownFileTypes ShownFileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Main window start-up position, size and state
|
||||
/// </summary>
|
||||
public WindowStartup WindowStartup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language Code for the UI
|
||||
/// </summary>
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable custom themes in the GUI
|
||||
/// </summary>
|
||||
public bool EnableCustomTheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to custom GUI theme
|
||||
/// </summary>
|
||||
public string CustomThemePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the base style // Not Used
|
||||
/// </summary>
|
||||
public string BaseStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the view mode of the game list // Not Used
|
||||
/// </summary>
|
||||
public int GameListViewMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show application name in Grid Mode // Not Used
|
||||
/// </summary>
|
||||
public bool ShowNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets App Icon Size // Not Used
|
||||
/// </summary>
|
||||
public int GridSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorts Apps in the game list // Not Used
|
||||
/// </summary>
|
||||
public int ApplicationSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets if Grid is ordered in Ascending Order // Not Used
|
||||
/// </summary>
|
||||
public bool IsAscendingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games in fullscreen mode
|
||||
/// </summary>
|
||||
public bool StartFullscreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show console window
|
||||
/// </summary>
|
||||
public bool ShowConsole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable keyboard support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public bool EnableKeyboard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable mouse support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public bool EnableMouse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hotkey Keyboard Bindings
|
||||
/// </summary>
|
||||
public KeyboardHotkeys Hotkeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy keyboard control bindings
|
||||
/// </summary>
|
||||
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
|
||||
/// TODO: Remove this when those older versions aren't in use anymore.
|
||||
public List<JsonObject> KeyboardConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy controller control bindings
|
||||
/// </summary>
|
||||
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
|
||||
/// TODO: Remove this when those older versions aren't in use anymore.
|
||||
public List<JsonObject> ControllerConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Input configurations
|
||||
/// </summary>
|
||||
public List<InputConfig> InputConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graphics backend
|
||||
/// </summary>
|
||||
public GraphicsBackend GraphicsBackend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preferred GPU
|
||||
/// </summary>
|
||||
public string PreferredGpu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer Mode
|
||||
/// </summary>
|
||||
public MultiplayerMode MultiplayerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// GUID for the network interface used by LAN (or 0 for default)
|
||||
/// </summary>
|
||||
public string MultiplayerLanInterfaceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses Hypervisor over JIT if available
|
||||
/// </summary>
|
||||
public bool UseHypervisor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration file from disk
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the JSON configuration file</param>
|
||||
/// <param name="configurationFileFormat">Parsed configuration file</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a configuration file to disk
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the JSON configuration file</param>
|
||||
public void SaveConfig(string path)
|
||||
{
|
||||
JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
1562
src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
Normal file
12
src/Ryujinx.UI.Common/Configuration/FileTypes.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Ryujinx.UI.Common
|
||||
{
|
||||
public enum FileTypes
|
||||
{
|
||||
NSP,
|
||||
PFS0,
|
||||
XCI,
|
||||
NCA,
|
||||
NRO,
|
||||
NSO
|
||||
}
|
||||
}
|
113
src/Ryujinx.UI.Common/Configuration/LoggerModule.cs
Normal file
|
@ -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<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Debug, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableStub(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Stub, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableInfo(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Info, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableWarning(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Warning, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableError(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Error, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableTrace(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Trace, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableGuest(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Guest, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadFilteredClasses(object sender, ReactiveEventArgs<LogClass[]> e)
|
||||
{
|
||||
bool noFilter = e.NewValue.Length == 0;
|
||||
|
||||
foreach (var logClass in Enum.GetValues<LogClass>())
|
||||
{
|
||||
Logger.SetEnable(logClass, noFilter);
|
||||
}
|
||||
|
||||
foreach (var logClass in e.NewValue)
|
||||
{
|
||||
Logger.SetEnable(logClass, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReloadFileLogger(object sender, ReactiveEventArgs<bool> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
src/Ryujinx.UI.Common/Configuration/System/Language.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.UI.Common.Configuration.System
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<Language>))]
|
||||
public enum Language
|
||||
{
|
||||
Japanese,
|
||||
AmericanEnglish,
|
||||
French,
|
||||
German,
|
||||
Italian,
|
||||
Spanish,
|
||||
Chinese,
|
||||
Korean,
|
||||
Dutch,
|
||||
Portuguese,
|
||||
Russian,
|
||||
Taiwanese,
|
||||
BritishEnglish,
|
||||
CanadianFrench,
|
||||
LatinAmericanSpanish,
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
}
|
||||
}
|
17
src/Ryujinx.UI.Common/Configuration/System/Region.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.UI.Common.Configuration.System
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<Region>))]
|
||||
public enum Region
|
||||
{
|
||||
Japan,
|
||||
USA,
|
||||
Europe,
|
||||
Australia,
|
||||
China,
|
||||
Korea,
|
||||
Taiwan,
|
||||
}
|
||||
}
|
8
src/Ryujinx.UI.Common/Configuration/UI/ColumnSort.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ryujinx.UI.Common.Configuration.UI
|
||||
{
|
||||
public struct ColumnSort
|
||||
{
|
||||
public int SortColumnId { get; set; }
|
||||
public bool SortAscending { get; set; }
|
||||
}
|
||||
}
|
16
src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
12
src/Ryujinx.UI.Common/Configuration/UI/ShownFileTypes.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
11
src/Ryujinx.UI.Common/Configuration/UI/WindowStartup.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
98
src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
Normal file
|
@ -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<bool> 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();
|
||||
}
|
||||
}
|
||||
}
|
25
src/Ryujinx.UI.Common/Extensions/FileTypeExtensions.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using static Ryujinx.UI.Common.Configuration.ConfigurationState.UISection;
|
||||
|
||||
namespace Ryujinx.UI.Common
|
||||
{
|
||||
public static class FileTypesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="ShownFileTypeSettings"/> value for the correlating FileType name.
|
||||
/// </summary>
|
||||
/// <param name="type">The name of the <see cref="ShownFileTypeSettings"/> parameter to get the value of.</param>
|
||||
/// <param name="config">The config instance to get the value from.</param>
|
||||
/// <returns>The current value of the setting. Value is <see langword="true"/> if the file type is the be shown on the games list, <see langword="false"/> otherwise.</returns>
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
99
src/Ryujinx.UI.Common/Helper/CommandLineState.cs
Normal file
|
@ -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<string> 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();
|
||||
}
|
||||
}
|
||||
}
|
50
src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
202
src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
62
src/Ryujinx.UI.Common/Helper/LinuxHelper.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
160
src/Ryujinx.UI.Common/Helper/ObjectiveC.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
src/Ryujinx.UI.Common/Helper/OpenHelper.cs
Normal file
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
src/Ryujinx.UI.Common/Helper/SetupValidator.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.UI.Common.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensure installation validity
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
162
src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
Normal file
|
@ -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<Rgba32>(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<Rgba32>(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<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
|
||||
{
|
||||
argsList.Add("--root-data-dir");
|
||||
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
|
||||
}
|
||||
|
||||
argsList.Add($"\"{appFilePath}\"");
|
||||
|
||||
return String.Join(" ", argsList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Icon (.ico) file using the source bitmap image at the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bitmap image that will be saved as an .ico file</param>
|
||||
/// <param name="filePath">The location that the new .ico file will be saved too (Make sure to include '.ico' in the path).</param>
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
30
src/Ryujinx.UI.Common/Helper/TitleHelper.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
219
src/Ryujinx.UI.Common/Helper/ValueFormatUtils.cs
Normal file
|
@ -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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="FormatFileSize"/>.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
|
||||
/// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable file size string.
|
||||
/// </summary>
|
||||
/// <param name="size">The file size in bytes.</param>
|
||||
/// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
|
||||
/// <returns>A human-readable file size string.</returns>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
|
||||
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
|
||||
/// <returns>A <see cref="long"/> representing a number of bytes.</returns>
|
||||
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
|
||||
}
|
||||
}
|
67
src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApi.cs
Normal file
|
@ -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<AmiiboApi>
|
||||
{
|
||||
[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<string, string> Release { get; set; }
|
||||
|
||||
[JsonPropertyName("gamesSwitch")]
|
||||
public List<AmiiboApiGamesSwitch> 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);
|
||||
}
|
||||
}
|
||||
}
|
15
src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs
Normal file
|
@ -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<AmiiboApiUsage> AmiiboUsage { get; set; }
|
||||
[JsonPropertyName("gameID")]
|
||||
public List<string> GameId { get; set; }
|
||||
[JsonPropertyName("gameName")]
|
||||
public string GameName { get; set; }
|
||||
}
|
||||
}
|
12
src/Ryujinx.UI.Common/Models/Amiibo/AmiiboApiUsage.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
14
src/Ryujinx.UI.Common/Models/Amiibo/AmiiboJson.cs
Normal file
|
@ -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<AmiiboApi> Amiibo { get; set; }
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.UI.Common.Models.Amiibo
|
||||
{
|
||||
[JsonSerializable(typeof(AmiiboJson))]
|
||||
public partial class AmiiboJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.UI.Common.Models.Github
|
||||
{
|
||||
public class GithubReleasesJsonResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<GithubReleaseAssetJsonResponse> Assets { get; set; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 81 KiB |
132
src/Ryujinx.UI.Common/Resources/Controller_ProCon.svg
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1050 1050.5"
|
||||
style="enable-background:new 0 0 1050 1050.5;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#20221F;}
|
||||
.st1{fill:#3B3B3B;}
|
||||
.st2{fill:#121212;}
|
||||
.st3{fill:#444542;}
|
||||
.st4{fill:#FFFFFF;}
|
||||
.st5{fill:#444542;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st6{fill:#454644;}
|
||||
.st7{fill:#454644;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st8{fill:#3B3C3A;}
|
||||
.st9{font-family:'Helvetica-Bold';}
|
||||
.st10{font-size:40px;}
|
||||
.st11{fill:#0D0D0A;}
|
||||
</style>
|
||||
<g id="Front">
|
||||
<path id="Right_Grip_00000028282943321285403220000008369785803052272051_" class="st0" d="M766,850.5
|
||||
c34,28.2,27.6,35.9,68.5,108.5c36.7,74.7,64.4,104.4,125.1,84.1v0c95.3-57.9,59.3-145.3,43.6-275.2c-10-60.6-35.6-190.3-35.6-190.3
|
||||
L766,850.5z"/>
|
||||
<path id="Left_Grip" class="st0" d="M82.3,577.6c0,0-25.6,129.7-35.6,190.3C31,897.8-5,985.1,90.3,1043v0
|
||||
c60.8,20.3,88.4-9.4,125.1-84.1c40.9-72.7,34.5-80.3,68.5-108.5L82.3,577.6z"/>
|
||||
<path id="Right_Bumper_00000006710349871522532470000011078040965381267594_" class="st1" d="M676.3,378.4
|
||||
c10.1-4.3,39.7-22.5,58.7-19.7c59.5,0.9,166.7,17.7,172.6,81.2"/>
|
||||
<path id="Left_Bumper_00000024680414077879639570000011759596763560342154_" class="st1" d="M142.4,439.9
|
||||
c5.9-63.4,113-80.2,172.6-81.2c19-2.8,48.6,15.4,58.7,19.7"/>
|
||||
<path id="Background_00000141418846164053065470000016150094984198570163_" class="st2" d="M766,850.5
|
||||
c35.5-30.8,68.5-74.7,96-113.5c26.9-36.3,94.7-136.7,105.6-159.3c0-2.4-6.3-30.1-12.8-56.2C919.1,361.9,702.2,378.1,525,378.1
|
||||
c-177.4,0-394.1-16.2-429.9,143.3c-6.5,26-12.8,53.8-12.8,56.2c10.9,22.6,78.8,123,105.6,159.3c27.5,38.8,60.5,82.8,96,113.5"/>
|
||||
<g id="Directional_Pad">
|
||||
<path id="Background_00000032628022449190479560000015279211462520783249_" class="st3" d="M466.2,683.5h-40c-2.8,0-5-2.2-5-5v-40
|
||||
c0-2.8-2.2-5-5-5h-30c-2.8,0-5,2.2-5,5v40c0,2.8-2.2,5-5,5h-40c-2.8,0-5,2.2-5,5v30c0,2.8,2.2,5,5,5h40c2.8,0,5,2.2,5,5v40
|
||||
c0,2.8,2.2,5,5,5h30c2.8,0,5-2.2,5-5v-40c0-2.8,2.2-5,5-5h40c2.8,0,5-2.2,5-5v-30C471.2,685.8,469,683.5,466.2,683.5z"/>
|
||||
<g id="Arrows">
|
||||
<g>
|
||||
<polygon class="st4" points="393.7,746 408.7,746 401.2,761 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st4" points="358.7,696 358.7,711 343.7,703.5 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st4" points="408.7,661 393.7,661 401.2,646 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st4" points="443.7,711 443.7,696 458.7,703.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="R_Thumbstick_00000152226188525111835500000011838297421350334865_">
|
||||
<circle id="Background_00000035532849542660068350000006517224202948159422_" class="st0" cx="650.6" cy="703.5" r="55"/>
|
||||
<circle id="Stick" class="st5" cx="650.6" cy="703.5" r="45"/>
|
||||
</g>
|
||||
<g id="L_Thumbstick_00000047032468231999382210000005512347386782594484_">
|
||||
<circle id="Background_00000182502673988292164000000007125719133096369561_" class="st0" cx="240.2" cy="564.8" r="55"/>
|
||||
<circle id="Stick_00000075121990265259598900000000214370239054002365_" class="st5" cx="240.2" cy="564.8" r="45"/>
|
||||
</g>
|
||||
<g id="Minus_Button">
|
||||
<circle id="_Background_00000120554951013892796430000015877571645746699662_" class="st6" cx="401" cy="489.3" r="22.5"/>
|
||||
<polyline id="Plus_00000039131319101621183460000006196023733899658629_" class="st4" points="386.2,491.8 386.2,486.8
|
||||
416.2,486.8 416.2,491.8 "/>
|
||||
</g>
|
||||
<g id="Plus_Button">
|
||||
<circle id="_Background" class="st6" cx="650.4" cy="489.6" r="22.5"/>
|
||||
<polygon id="Plus" class="st4" points="665.6,487.1 653.1,487.1 653.1,474.4 648.1,474.4 648.1,487.1 635.6,487.1 635.6,492.1
|
||||
648.1,492.1 648.1,504.4 653.1,504.4 653.1,492.1 665.6,492.1 "/>
|
||||
</g>
|
||||
<g id="Home_Button_00000029758737660217614780000001403165237001195407_">
|
||||
<circle id="_Background_00000132788487854287834010000009548421243227981499_" class="st6" cx="605.4" cy="564.8" r="22.5"/>
|
||||
<path id="Home" class="st4" d="M605.4,549.8l-15,15h5v15h20v-15h5L605.4,549.8z M610.4,574.8h-10v-10h10V574.8z"/>
|
||||
</g>
|
||||
<g id="Capture_Button_00000105394663133565750060000017455731898661404072_">
|
||||
<path class="st6" d="M468.6,586.5h-30c-2.8,0-5-2.2-5-5v-29.5c0-2.8,2.2-5,5-5h30c2.8,0,5,2.2,5,5v29.5
|
||||
C473.6,584.2,471.4,586.5,468.6,586.5z"/>
|
||||
<circle class="st7" cx="453.6" cy="566.7" r="15"/>
|
||||
</g>
|
||||
<g id="Buttons_00000023239109225132251950000005218343074279628213_">
|
||||
<g id="A_Button">
|
||||
<circle id="Background_00000006699118933065716380000004636085088820886913_" class="st8" cx="863.9" cy="564.8" r="35"/>
|
||||
<text transform="matrix(1 0 0 1 849.4224 578.6607)" class="st4 st9 st10">A</text>
|
||||
</g>
|
||||
<g id="X_Button">
|
||||
<circle id="Background_00000083074713085756701790000016893839312974798515_" class="st8" cx="793.9" cy="494.8" r="35"/>
|
||||
<text transform="matrix(1 0 0 1 780.5266 508.6604)" class="st4 st9 st10">X</text>
|
||||
</g>
|
||||
<g id="Y_Button_00000100344340438574137780000014238704828967683973_">
|
||||
<circle id="Background_00000137100455694543496620000011124722786613194377_" class="st8" cx="723.9" cy="564.8" r="35"/>
|
||||
<text transform="matrix(1 0 0 1 710.5263 578.661)" class="st4 st9 st10">Y</text>
|
||||
</g>
|
||||
<g id="B_Button_00000041994261956088037220000013468634544777304733_">
|
||||
<circle id="Background_00000096038108578846046800000001873940014252420514_" class="st8" cx="793.9" cy="634.8" r="35"/>
|
||||
<text transform="matrix(1 0 0 1 780.9706 648.6605)" class="st4 st9 st10">B</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Top_Down">
|
||||
<path id="Left_Grip_00000026131988385328425370000016677941743356253314_" class="st0" d="M219.2,78.5
|
||||
c-12.5-17.6-25.9-42.3-45.6-58.6C153.5,3.3,112.1-4.7,87.1,5.8c-13.9,5.8-33.4,33.1-42.7,52.8C33.9,80.9,30.4,109.9,32,141.4
|
||||
c1.2,25.1,5.3,51.7,14.2,78.6c0,0,14.3,53.8,42.8,80.8c11.2,10.6,35,26.6,35,26.6l116-217.5C240,109.9,224.6,86.2,219.2,78.5z"/>
|
||||
<path id="Right_Grip_00000016782759094708820330000002450847065936193693_" class="st0" d="M828.6,78.5
|
||||
c12.5-17.6,25.9-42.3,45.6-58.6c20.1-16.6,61.4-24.5,86.5-14.1c13.9,5.8,33.4,33.1,42.7,52.8c10.5,22.3,13.9,51.3,12.4,82.8
|
||||
c-1.2,25.1-5.3,51.7-14.2,78.6c0,0-14.3,53.8-42.8,80.8c-11.2,10.6-35,26.6-35,26.6l-116-217.5C807.8,109.9,823.2,86.2,828.6,78.5z
|
||||
"/>
|
||||
<path id="Background_00000169534857628063347190000007586592143875928969_" class="st11" d="M866,122.2
|
||||
c66.3,18.7,85.1,128.8,69,186c-2.5,54.2-148.9,15.3-265.1,31.2c-41.1,1.7-91.8,2.4-145.9,2.3c-54.1,0-104.8-0.6-145.9-2.3
|
||||
c-116.2-15.9-262.6,23.1-265.1-31.2c-16.1-57.1,2.6-167.3,69-186l60.5-18.8l38.9-1.9c40.2,0.1,142.8,0,242.7,0
|
||||
c99.9,0,202.4,0.1,242.7,0l38.9,1.9L866,122.2z"/>
|
||||
<g id="ZL_Trigger_00000005254517714433203260000014117442438696169895_">
|
||||
<path id="Background_00000111870097528015387240000017384507710402295183_" class="st1" d="M145.9,239.2
|
||||
c15.2-97.4,38.1-147.2,141.7-137c8.2,16.4,43.3,83,50.6,105.7C280.6,227.2,204.7,225.6,145.9,239.2z"/>
|
||||
<text id="ZL_Trigger" transform="matrix(1.0139 0 0 1 218.3906 179.3992)" class="st4 st9 st10">ZL</text>
|
||||
</g>
|
||||
<g id="ZR_Trigger">
|
||||
<path id="Background_00000133526766189752063450000016781240006605114763_" class="st1" d="M716.2,207.9
|
||||
c7.4-22.7,42.5-89.3,50.6-105.7c103.7-10.2,126.5,39.6,141.7,137C849.8,225.6,773.8,227.2,716.2,207.9z"/>
|
||||
|
||||
<text id="ZL_Trigger_00000000206350378518266660000001315160307759857328_" transform="matrix(1.0139 0 0 1 784.2356 179.3992)" class="st4 st9 st10">ZR</text>
|
||||
</g>
|
||||
<g id="R_Trigger_00000085939413106284991650000014018840000393673094_">
|
||||
<path id="Background" class="st1" d="M664,318.5c7-10.1,27.8-78.4,45.4-78.7C1040.8,243.7,897.1,334,664,318.5z"/>
|
||||
<text id="R_Trigger" transform="matrix(1 0 0 1 769.6461 292.8947)" class="st4 st9 st10">R</text>
|
||||
</g>
|
||||
<g id="L_Trigger">
|
||||
<path id="Background_00000043427985111927735300000011910735497762731703_" class="st1" d="M340.6,238.6
|
||||
c17.6,0.3,38.4,68.6,45.4,78.7C152.9,332.8,9.2,242.6,340.6,238.6z"/>
|
||||
|
||||
<text id="R_Trigger_00000092444210070373642420000009814634285137007748_" transform="matrix(1 0 0 1 253.7327 291.7279)" class="st4 st9 st10">L</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Icon_NCA.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Icon_NRO.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Icon_NSO.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Icon_NSP.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Icon_XCI.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Amiibo.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Discord_Dark.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Discord_Light.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_GitHub_Dark.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_GitHub_Light.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Patreon_Dark.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Patreon_Light.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Ryujinx.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Twitter_Dark.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/Ryujinx.UI.Common/Resources/Logo_Twitter_Light.png
Normal file
After Width: | Height: | Size: 19 KiB |
68
src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj
Normal file
|
@ -0,0 +1,68 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Controller_JoyConLeft.svg" />
|
||||
<None Remove="Resources\Controller_JoyConPair.svg" />
|
||||
<None Remove="Resources\Controller_JoyConRight.svg" />
|
||||
<None Remove="Resources\Controller_ProCon.svg" />
|
||||
<None Remove="Resources\Icon_NCA.png" />
|
||||
<None Remove="Resources\Icon_NRO.png" />
|
||||
<None Remove="Resources\Icon_NSO.png" />
|
||||
<None Remove="Resources\Icon_NSP.png" />
|
||||
<None Remove="Resources\Icon_XCI.png" />
|
||||
<None Remove="Resources\Logo_Amiibo.png" />
|
||||
<None Remove="Resources\Logo_Discord.png" />
|
||||
<None Remove="Resources\Logo_GitHub.png" />
|
||||
<None Remove="Resources\Logo_Patreon.png" />
|
||||
<None Remove="Resources\Logo_Ryujinx.png" />
|
||||
<None Remove="Resources\Logo_Twitter.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Controller_JoyConLeft.svg" />
|
||||
<EmbeddedResource Include="Resources\Controller_JoyConPair.svg" />
|
||||
<EmbeddedResource Include="Resources\Controller_JoyConRight.svg" />
|
||||
<EmbeddedResource Include="Resources\Controller_ProCon.svg" />
|
||||
<EmbeddedResource Include="Resources\Icon_NCA.png" />
|
||||
<EmbeddedResource Include="Resources\Icon_NRO.png" />
|
||||
<EmbeddedResource Include="Resources\Icon_NSO.png" />
|
||||
<EmbeddedResource Include="Resources\Icon_NSP.png" />
|
||||
<EmbeddedResource Include="Resources\Icon_XCI.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Amiibo.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Ryujinx.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Discord_Dark.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Discord_Light.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_GitHub_Dark.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_GitHub_Light.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Patreon_Dark.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Patreon_Light.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Twitter_Dark.png" />
|
||||
<EmbeddedResource Include="Resources\Logo_Twitter_Light.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64' OR '$(RuntimeIdentifier)' == ''">
|
||||
<EmbeddedResource Include="..\..\distribution\linux\shortcut-template.desktop" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64' OR '$(RuntimeIdentifier)' == ''">
|
||||
<EmbeddedResource Include="..\..\distribution\macos\shortcut-template.plist" />
|
||||
<EmbeddedResource Include="..\..\distribution\macos\shortcut-launch-script.sh" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscordRichPresence" />
|
||||
<PackageReference Include="securifybv.ShellLink" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
85
src/Ryujinx.UI.Common/SystemInfo/LinuxSystemInfo.cs
Normal file
|
@ -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<string, string>(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<string, string>(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<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
164
src/Ryujinx.UI.Common/SystemInfo/MacOSSystemInfo.cs
Normal file
|
@ -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<VMStatistics64>() / 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<T>(string name, ref T oldValue)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
ulong oldValueSize = (ulong)Unsafe.SizeOf<T>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
79
src/Ryujinx.UI.Common/SystemInfo/SystemInfo.cs
Normal file
|
@ -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<int, byte>(regs)).Replace('\0', ' ').Trim();
|
||||
|
||||
return string.IsNullOrEmpty(name) ? null : name;
|
||||
}
|
||||
}
|
||||
}
|
87
src/Ryujinx.UI.Common/SystemInfo/WindowsSystemInfo.cs
Normal file
|
@ -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<MemoryStatusEx>();
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
39
src/Ryujinx.UI.Common/UserError.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
namespace Ryujinx.UI.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a common error that could be reported to the user by the emulator.
|
||||
/// </summary>
|
||||
public enum UserError
|
||||
{
|
||||
/// <summary>
|
||||
/// No error to report.
|
||||
/// </summary>
|
||||
Success = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// No keys are present.
|
||||
/// </summary>
|
||||
NoKeys = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// No firmware is installed.
|
||||
/// </summary>
|
||||
NoFirmware = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// Firmware parsing failed.
|
||||
/// </summary>
|
||||
/// <remarks>Most likely related to keys.</remarks>
|
||||
FirmwareParsingFailed = 0x3,
|
||||
|
||||
/// <summary>
|
||||
/// No application was found at the given path.
|
||||
/// </summary>
|
||||
ApplicationNotFound = 0x4,
|
||||
|
||||
/// <summary>
|
||||
/// An unknown error.
|
||||
/// </summary>
|
||||
Unknown = 0xDEAD,
|
||||
}
|
||||
}
|