Move solution and projects to src
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; }
|
||||
}
|
||||
}
|
23
src/Ryujinx.Ui.Common/App/ApplicationData.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using LibHac.Common;
|
||||
using LibHac.Ns;
|
||||
|
||||
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 string TimePlayed { get; set; }
|
||||
public double TimePlayedNum { get; set; }
|
||||
public string LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public double FileSizeBytes { get; set; }
|
||||
public string Path { get; set; }
|
||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
923
src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
Normal file
|
@ -0,0 +1,923 @@
|
|||
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.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.Linq;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
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 \"Config.json\" 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;
|
||||
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;
|
||||
}
|
||||
|
||||
double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
|
||||
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
|
||||
{
|
||||
PartitionFileSystem pfs;
|
||||
|
||||
bool isExeFs = false;
|
||||
|
||||
if (extension == ".xci")
|
||||
{
|
||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
pfs = new PartitionFileSystem(file.AsStorage());
|
||||
|
||||
// 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
|
||||
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
if (appMetadata.LastPlayed != "Never")
|
||||
{
|
||||
if (!DateTime.TryParse(appMetadata.LastPlayed, out _))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)");
|
||||
|
||||
appMetadata.LastPlayed = "Never";
|
||||
}
|
||||
else
|
||||
{
|
||||
appMetadata.LastPlayed = appMetadata.LastPlayed[..^3];
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationData data = new()
|
||||
{
|
||||
Favorite = appMetadata.Favorite,
|
||||
Icon = applicationIcon,
|
||||
TitleName = titleName,
|
||||
TitleId = titleId,
|
||||
Developer = developer,
|
||||
Version = version,
|
||||
TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
|
||||
TimePlayedNum = appMetadata.TimePlayed,
|
||||
LastPlayed = appMetadata.LastPlayed,
|
||||
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
|
||||
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
|
||||
FileSizeBytes = 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(PartitionFileSystem 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 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)
|
||||
{
|
||||
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
|
||||
{
|
||||
PartitionFileSystem pfs;
|
||||
|
||||
bool isExeFs = false;
|
||||
|
||||
if (extension == ".xci")
|
||||
{
|
||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
pfs = new PartitionFileSystem(file.AsStorage());
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
||||
{
|
||||
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
|
||||
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
|
||||
}
|
||||
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 static string ConvertSecondsToFormattedString(double seconds)
|
||||
{
|
||||
System.TimeSpan time = System.TimeSpan.FromSeconds(seconds);
|
||||
|
||||
string timeString;
|
||||
if (time.Days != 0)
|
||||
{
|
||||
timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Hours != 0)
|
||||
{
|
||||
timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Minutes != 0)
|
||||
{
|
||||
timeString = $"{time.Minutes:D2}m";
|
||||
}
|
||||
else
|
||||
{
|
||||
timeString = "Never";
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
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, PartitionFileSystem 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 Nca(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 Nca(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 FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||
|
||||
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
public class ApplicationMetadata
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public double TimePlayed { get; set; }
|
||||
public string LastPlayed { get; set; } = "Never";
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
}
|
393
src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
Normal file
|
@ -0,0 +1,393 @@
|
|||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
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 = 46;
|
||||
|
||||
/// <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>
|
||||
/// Hide Cursor on Idle
|
||||
/// </summary>
|
||||
public bool HideCursorOnIdle { 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 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>
|
||||
/// 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>
|
||||
/// 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
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Ryujinx.Ui.Common.Configuration
|
||||
{
|
||||
public enum ConfigurationLoadResult
|
||||
{
|
||||
Success = 0,
|
||||
NotLoaded = 1,
|
||||
MigratedFromPreVulkan = 1 << 8
|
||||
}
|
||||
}
|
1456
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
|
||||
}
|
||||
}
|
94
src/Ryujinx.Ui.Common/Configuration/LoggerModule.cs
Normal file
|
@ -0,0 +1,94 @@
|
|||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
|
||||
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; }
|
||||
}
|
||||
}
|
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 Button[]
|
||||
{
|
||||
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 Button[]
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
88
src/Ryujinx.Ui.Common/Helper/CommandLineState.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
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 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;
|
||||
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);
|
||||
}
|
||||
}
|
198
src/Ryujinx.Ui.Common/Helper/FileAssociationHelper.cs
Normal file
|
@ -0,0 +1,198 @@
|
|||
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 string[] _fileExtensions = new string[] { ".nca", ".nro", ".nso", ".nsp", ".xci" };
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static 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 (!AreMimeTypesRegisteredLinux())
|
||||
{
|
||||
string mimeTypesFile = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "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;
|
||||
}
|
||||
|
||||
key.OpenSubKey(@"shell\open\command");
|
||||
|
||||
string keyValue = (string)key.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 (!AreMimeTypesRegisteredWindows())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Registry.CurrentUser.DeleteSubKeyTree(keyString);
|
||||
}
|
||||
else
|
||||
{
|
||||
RegistryKey key = Registry.CurrentUser.CreateSubKey(keyString);
|
||||
if (key is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
key.CreateSubKey(@"shell\open\command");
|
||||
|
||||
key.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\"");
|
||||
key.Close();
|
||||
}
|
||||
|
||||
// Notify Explorer the file association has been changed.
|
||||
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool registered = false;
|
||||
|
||||
foreach (string ext in _fileExtensions)
|
||||
{
|
||||
registered |= RegisterExtension(ext, uninstall);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
97
src/Ryujinx.Ui.Common/Helper/ObjectiveC.cs
Normal file
|
@ -0,0 +1,97 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
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 unsafe partial IntPtr sel_getUid(string name);
|
||||
|
||||
[LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)]
|
||||
public static partial IntPtr objc_getClass(string name);
|
||||
|
||||
[LibraryImport(ObjCRuntime)]
|
||||
public static partial void objc_msgSend(IntPtr receiver, Selector selector);
|
||||
|
||||
[LibraryImport(ObjCRuntime)]
|
||||
public static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value);
|
||||
|
||||
[LibraryImport(ObjCRuntime)]
|
||||
public static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value);
|
||||
|
||||
[LibraryImport(ObjCRuntime)]
|
||||
public static partial void objc_msgSend(IntPtr receiver, Selector selector, NSRect point);
|
||||
|
||||
[LibraryImport(ObjCRuntime)]
|
||||
public static partial void objc_msgSend(IntPtr receiver, Selector selector, double value);
|
||||
|
||||
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
|
||||
public static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector);
|
||||
|
||||
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
|
||||
public static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param);
|
||||
|
||||
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend", StringMarshalling = StringMarshalling.Utf8)]
|
||||
public static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector, string param);
|
||||
|
||||
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool bool_objc_msgSend(IntPtr receiver, Selector selector, IntPtr param);
|
||||
|
||||
public struct Selector
|
||||
{
|
||||
public readonly IntPtr SelPtr;
|
||||
|
||||
public unsafe Selector(string name)
|
||||
{
|
||||
SelPtr = sel_getUid(name);
|
||||
}
|
||||
|
||||
public static implicit operator Selector(string value) => new(value);
|
||||
}
|
||||
|
||||
public 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)]
|
||||
public static partial int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags);
|
||||
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
public static partial void ILFree(IntPtr pidlList);
|
||||
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
public 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);
|
||||
IntPtr nsUrl = ObjectiveC.objc_getClass("NSURL");
|
||||
var urlPtr = ObjectiveC.IntPtr_objc_msgSend(nsUrl, "fileURLWithPath:", nsStringPath);
|
||||
|
||||
IntPtr nsArray = ObjectiveC.objc_getClass("NSArray");
|
||||
IntPtr urlArray = ObjectiveC.IntPtr_objc_msgSend(nsArray, "arrayWithObject:", urlPtr);
|
||||
|
||||
IntPtr nsWorkspace = ObjectiveC.objc_getClass("NSWorkspace");
|
||||
IntPtr sharedWorkspace = ObjectiveC.IntPtr_objc_msgSend(nsWorkspace, "sharedWorkspace");
|
||||
|
||||
ObjectiveC.objc_msgSend(sharedWorkspace, "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);
|
||||
IntPtr nsUrl = ObjectiveC.objc_getClass("NSURL");
|
||||
var urlPtr = ObjectiveC.IntPtr_objc_msgSend(nsUrl, "URLWithString:", nsStringPath);
|
||||
|
||||
IntPtr nsWorkspace = ObjectiveC.objc_getClass("NSWorkspace");
|
||||
IntPtr sharedWorkspace = ObjectiveC.IntPtr_objc_msgSend(nsWorkspace, "sharedWorkspace");
|
||||
|
||||
ObjectiveC.bool_objc_msgSend(sharedWorkspace, "openURL:", urlPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Cannot open url \"{url}\" on this platform!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
118
src/Ryujinx.Ui.Common/Helper/SetupValidator.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
error = UserError.ApplicationNotFound;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
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 override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public string GetId()
|
||||
{
|
||||
return Head + Tail;
|
||||
}
|
||||
|
||||
public bool Equals(AmiiboApi other)
|
||||
{
|
||||
return Head + Tail == other.Head + other.Tail;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is AmiiboApi other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Head, Tail);
|
||||
}
|
||||
}
|
||||
}
|
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 |
1
src/Ryujinx.Ui.Common/Resources/Controller_ProCon.svg
Normal file
After Width: | Height: | Size: 59 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 |
57
src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
Normal file
|
@ -0,0 +1,57 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.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>
|
||||
<PackageReference Include="DiscordRichPresence" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
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
|
||||
}
|
||||
}
|