aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.UI.Common
diff options
context:
space:
mode:
authorTSRBerry <20988865+TSRBerry@users.noreply.github.com>2024-07-16 23:17:32 +0200
committerGitHub <noreply@github.com>2024-07-16 18:17:32 -0300
commit6fbf279faca30ffd2ef33394463b98809ccaf127 (patch)
tree3054ed7c4b2a2d806375297476ab8f6df8fc467c /src/Ryujinx.UI.Common
parent344f4f52c1117028d08802aff60fbd4d875717b4 (diff)
Add support for multi game XCIs (second try) (#6515)
* Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Apply suggestions from code review Co-authored-by: gdkchan <gab.dark.100@gmail.com> * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again * Don't add duplicates to update/dlc windows * Avoid double lookup * Preserve DLC enabled state for bundled DLCs * Fix DLCWindow not opening using GTK * Fix missing information when loading applications from file * Address review feedback Rename ContentCollection to ContentMetaData Fix casing issues in log messages Use null as the default value for updatePath * Fix re-adding bundled DLCs every time * Fix bundled DLCs disappearing * Abstract common code to open application pfs * Remove unused imports * Fix file exists check when loading DLCs * Load bundled DLCs only using dlc.json * Load AoC items correctly * Add all DLCs from a PFS * Add argument to launch a specific application id * Use application-id argument for shortcuts if necessary * Return the application id from the control NCA if possible * GetApplicationInformation: Don't overwrite application ids Move SaveDataOwnerId check to the top, since it seems to be more reliable. * Get application ids from CNMT again This commit reverts some parts of 61615b8f0d6f90ae86778958ddc38eaf6dc280ab. Since the issue wasn't actually related to the application id in CMNTs, we can remove the wrong assumptions. * Revert erroneous axaml change from adca8900 * Rename title to application * Wrap nsp/pfs0 case with curly braces * Check if _applicationData.ControlHolder.ByteSpan is zeros only once * Catch exceptions while loading applications from nsps --------- Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Diffstat (limited to 'src/Ryujinx.UI.Common')
-rw-r--r--src/Ryujinx.UI.Common/App/ApplicationData.cs18
-rw-r--r--src/Ryujinx.UI.Common/App/ApplicationLibrary.cs854
-rw-r--r--src/Ryujinx.UI.Common/Helper/CommandLineState.cs5
-rw-r--r--src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs26
4 files changed, 457 insertions, 446 deletions
diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs
index 13c05655..7108defc 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs
@@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Helper;
using System;
using System.IO;
+using System.Text.Json.Serialization;
namespace Ryujinx.UI.App.Common
{
@@ -19,10 +21,10 @@ namespace Ryujinx.UI.App.Common
{
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 Name { get; set; } = "Unknown";
+ public ulong Id { get; set; }
+ public string Developer { get; set; } = "Unknown";
+ public string Version { get; set; } = "0";
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
@@ -36,7 +38,11 @@ namespace Ryujinx.UI.App.Common
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
- public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
+ [JsonIgnore] public string IdString => Id.ToString("x16");
+
+ [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
+
+ public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
@@ -105,7 +111,7 @@ namespace Ryujinx.UI.App.Common
return string.Empty;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
if (updatePatchNca != null)
{
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 176011dd..ef3826cf 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -4,28 +4,29 @@ using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
+using LibHac.Ncm;
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.HLE.Loaders.Processes.Extensions;
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 ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using TimeSpan = System.TimeSpan;
@@ -43,15 +44,16 @@ namespace Ryujinx.UI.App.Common
private readonly byte[] _nsoIcon;
private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly IntegrityCheckLevel _checkLevel;
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)
+ public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
_virtualFileSystem = virtualFileSystem;
+ _checkLevel = checkLevel;
_nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png");
_xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png");
@@ -70,272 +72,251 @@ namespace Ryujinx.UI.App.Common
return resourceByteArray;
}
- public void CancelLoading()
+ private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath)
{
- _cancellationToken?.Cancel();
- }
+ ApplicationData data = new()
+ {
+ Icon = _nspIcon,
+ };
- public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty)
- {
- using UniqueRef<IFile> controlFile = new();
+ using UniqueRef<IFile> npdmFile = new();
- controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
- }
+ try
+ {
+ Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
- public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage)
- {
- int numApplicationsFound = 0;
- int numApplicationsLoaded = 0;
+ if (ResultFs.PathNotFound.Includes(result))
+ {
+ Npdm npdm = new(npdmFile.Get.AsStream());
- _desiredTitleLanguage = desiredTitleLanguage;
+ data.Name = npdm.TitleName;
+ data.Id = npdm.Aci0.TitleId;
+ }
- _cancellationToken = new CancellationTokenSource();
+ return data;
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}");
- // Builds the applications list with paths to found applications
- List<string> applications = new();
+ return null;
+ }
+ }
- try
- {
- foreach (string appDir in appDirs)
- {
- if (_cancellationToken.Token.IsCancellationRequested)
- {
- return;
- }
+ private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath)
+ {
+ bool isExeFs = false;
- if (!Directory.Exists(appDir))
- {
- Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist.");
+ // 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;
- continue;
- }
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
+ {
+ if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca")
+ {
+ using UniqueRef<IFile> ncaFile = new();
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);
- });
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- 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;
+ Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
+ int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
- if (!File.Exists(fullPath))
- {
- Logger.Warning?.Print(LogClass.Application, $"Skipping invalid symlink: {fileInfo.FullName}");
- continue;
- }
+ // 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;
- applications.Add(fullPath);
- numApplicationsFound++;
- }
+ break;
}
}
- catch (UnauthorizedAccessException)
+ catch (Exception exception)
{
- Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\"");
+ Logger.Warning?.Print(LogClass.Application, $"Encountered an error while trying to load applications from file '{filePath}': {exception}");
+
+ return null;
}
}
-
- // Loops through applications list, creating a struct and then firing an event containing the struct for each application
- foreach (string applicationPath in applications)
+ else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
{
- if (_cancellationToken.Token.IsCancellationRequested)
- {
- return;
- }
+ isExeFs = true;
+ }
+ }
- long fileSize = new FileInfo(applicationPath).Length;
- string titleName = "Unknown";
- string titleId = "0000000000000000";
- string developer = "Unknown";
- string version = "0";
- byte[] applicationIcon = null;
+ if (hasMainNca)
+ {
+ List<ApplicationData> applications = GetApplicationsFromPfs(pfs, filePath);
- BlitStruct<ApplicationControlProperty> controlHolder = new(1);
+ switch (applications.Count)
+ {
+ case 1:
+ return applications[0];
+ case >= 1:
+ Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}");
+ return applications[0];
+ default:
+ return null;
+ }
+ }
- try
- {
- string extension = Path.GetExtension(applicationPath).ToLower();
+ if (isExeFs)
+ {
+ return GetApplicationFromExeFs(pfs, filePath);
+ }
- using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
+ return null;
+ }
- if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
- {
- try
- {
- IFileSystem pfs;
+ private List<ApplicationData> GetApplicationsFromPfs(IFileSystem pfs, string filePath)
+ {
+ var applications = new List<ApplicationData>();
+ string extension = Path.GetExtension(filePath).ToLower();
- bool isExeFs = false;
+ foreach ((ulong titleId, ContentMetaData content) in pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel))
+ {
+ ApplicationData applicationData = new()
+ {
+ Id = titleId,
+ Path = filePath,
+ };
- if (extension == ".xci")
- {
- Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
+ try
+ {
+ Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program);
+ Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control);
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- var pfsTemp = new PartitionFileSystem();
- pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
- pfs = pfsTemp;
+ BlitStruct<ApplicationControlProperty> controlHolder = new(1);
- // 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;
+ IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
- {
- if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
- {
- using UniqueRef<IFile> ncaFile = new();
+ // Check if there is an update available.
+ if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs))
+ {
+ // Replace the original ControlFs by the updated one.
+ controlFs = updatedControlFs;
+ }
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ ReadControlData(controlFs, controlHolder.ByteSpan);
- Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+ GetApplicationInformation(ref controlHolder.Value, ref applicationData);
- // 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;
+ // Read the icon from the ControlFS and store it as a byte array
+ try
+ {
+ using UniqueRef<IFile> icon = new();
- break;
- }
- }
- else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
- {
- isExeFs = true;
- }
- }
+ controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- if (!hasMainNca && !isExeFs)
- {
- numApplicationsFound--;
+ using MemoryStream stream = new();
- continue;
- }
- }
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
+ }
+ catch (HorizonResultException)
+ {
+ foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
+ {
+ if (entry.Name == "control.nacp")
+ {
+ continue;
+ }
- if (isExeFs)
- {
- applicationIcon = _nspIcon;
+ using var icon = new UniqueRef<IFile>();
- using UniqueRef<IFile> npdmFile = new();
+ controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
+ using MemoryStream stream = new();
- if (ResultFs.PathNotFound.Includes(result))
- {
- Npdm npdm = new(npdmFile.Get.AsStream());
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
- titleName = npdm.TitleName;
- titleId = npdm.Aci0.TitleId.ToString("x16");
- }
- }
- else
- {
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
+ if (applicationData.Icon != null)
+ {
+ break;
+ }
+ }
- // Check if there is an update available.
- if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
- {
- // Replace the original ControlFs by the updated one.
- controlFs = updatedControlFs;
- }
+ applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon;
+ }
- ReadControlData(controlFs, controlHolder.ByteSpan);
+ applicationData.ControlHolder = controlHolder;
- GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
+ applications.Add(applicationData);
+ }
+ catch (MissingKeyException exception)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
- // Read the icon from the ControlFS and store it as a byte array
- try
- {
- using UniqueRef<IFile> icon = new();
+ Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
+ }
+ catch (InvalidDataException)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
- controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}");
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}");
+ }
+ }
- using MemoryStream stream = new();
+ return applications;
+ }
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
- catch (HorizonResultException)
- {
- foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
- {
- if (entry.Name == "control.nacp")
- {
- continue;
- }
+ public bool TryGetApplicationsFromFile(string applicationPath, out List<ApplicationData> applications)
+ {
+ applications = [];
+
+ long fileSize = new FileInfo(applicationPath).Length;
- using var icon = new UniqueRef<IFile>();
+ BlitStruct<ApplicationControlProperty> controlHolder = new(1);
- controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ try
+ {
+ string extension = Path.GetExtension(applicationPath).ToLower();
- using MemoryStream stream = new();
+ using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
+ switch (extension)
+ {
+ case ".xci":
+ {
+ Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
- if (applicationIcon != null)
- {
- break;
- }
- }
+ applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath);
- applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
- }
- }
- }
- catch (MissingKeyException exception)
+ if (applications.Count == 0)
{
- applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
-
- Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
+ return false;
}
- 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}");
+ break;
+ }
+ case ".nsp":
+ case ".pfs0":
+ {
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(file.AsStorage()).ThrowIfFailure();
- numApplicationsFound--;
+ ApplicationData result = GetApplicationFromNsp(pfs, applicationPath);
- continue;
+ if (result == null)
+ {
+ return false;
}
+
+ applications.Add(result);
+
+ break;
}
- else if (extension == ".nro")
+ case ".nro":
{
BinaryReader reader = new(file);
-
- byte[] Read(long position, int size)
- {
- file.Seek(position, SeekOrigin.Begin);
-
- return reader.ReadBytes(size);
- }
+ ApplicationData application = new();
try
{
@@ -356,46 +337,61 @@ namespace Ryujinx.UI.App.Common
// Reads and stores game icon as byte array
if (iconSize > 0)
{
- applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
+ application.Icon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
- applicationIcon = _nroIcon;
+ application.Icon = _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);
+ GetApplicationInformation(ref controlHolder.Value, ref application);
}
else
{
- applicationIcon = _nroIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ application.Icon = _nroIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
}
+
+ application.ControlHolder = controlHolder;
+ applications.Add(application);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
+ return false;
+ }
- continue;
+ break;
+
+ byte[] Read(long position, int size)
+ {
+ file.Seek(position, SeekOrigin.Begin);
+
+ return reader.ReadBytes(size);
}
}
- else if (extension == ".nca")
+ case ".nca":
{
try
{
+ ApplicationData application = new();
+
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()))
+ if (!nca.IsProgram() || nca.IsPatch())
{
- numApplicationsFound--;
-
- continue;
+ return false;
}
+
+ application.Icon = _ncaIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
+ application.ControlHolder = controlHolder;
+
+ applications.Add(application);
}
catch (InvalidDataException)
{
@@ -405,78 +401,178 @@ namespace Ryujinx.UI.App.Common
{
Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
-
- continue;
+ return false;
}
- applicationIcon = _ncaIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ break;
+ }
+ // If its an NSO we just set defaults
+ case ".nso":
+ {
+ ApplicationData application = new()
+ {
+ Icon = _nsoIcon,
+ Name = Path.GetFileNameWithoutExtension(applicationPath),
+ };
+
+ applications.Add(application);
+ break;
}
- // If its an NSO we just set defaults
- else if (extension == ".nso")
+ }
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, exception.Message);
+
+ return false;
+ }
+
+ foreach (var data in applications)
+ {
+ ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
+ {
+ appMetadata.Title = data.Name;
+
+ // 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))
{
- applicationIcon = _nsoIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
+ appMetadata.LastPlayed = lastPlayedOldParsed;
+
+ // Migration successful: deleting last_played from the metadata file.
+ appMetadata.LastPlayedOld = default;
}
+
}
- catch (IOException exception)
+ });
+
+ data.Favorite = appMetadata.Favorite;
+ data.TimePlayed = appMetadata.TimePlayed;
+ data.LastPlayed = appMetadata.LastPlayed;
+ data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();
+ data.FileSize = fileSize;
+ data.Path = applicationPath;
+ }
+
+ return true;
+ }
+
+ 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> applicationPaths = new();
+
+ try
+ {
+ foreach (string appDir in appDirs)
+ {
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- Logger.Warning?.Print(LogClass.Application, exception.Message);
+ return;
+ }
- numApplicationsFound--;
+ if (!Directory.Exists(appDir))
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist.");
continue;
}
- ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
+ try
{
- 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)
+ IEnumerable<string> files = Directory.EnumerateFiles(appDir, "*", SearchOption.AllDirectories).Where(file =>
{
- appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
- appMetadata.TimePlayedOld = default;
- }
+ 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);
+ });
- // 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)
+ foreach (string app in files)
{
- // Migrate from string-based last_played to DateTime-based last_played_utc.
- if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- appMetadata.LastPlayed = lastPlayedOldParsed;
-
- // Migration successful: deleting last_played from the metadata file.
- appMetadata.LastPlayedOld = default;
+ 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;
+ applicationPaths.Add(fullPath);
+ numApplicationsFound++;
+ }
}
- });
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{appDir}\"");
+ }
+ }
- ApplicationData data = new()
+ // Loops through applications list, creating a struct and then firing an event containing the struct for each application
+ foreach (string applicationPath in applicationPaths)
+ {
+ if (_cancellationToken.Token.IsCancellationRequested)
{
- 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
+ return;
+ }
+
+ if (TryGetApplicationsFromFile(applicationPath, out List<ApplicationData> applications))
{
- AppData = data,
- });
+ foreach (var application in applications)
+ {
+ OnApplicationAdded(new ApplicationAddedEventArgs
+ {
+ AppData = application,
+ });
+ }
+
+ if (applications.Count > 1)
+ {
+ numApplicationsFound += applications.Count - 1;
+ }
+
+ numApplicationsLoaded += applications.Count;
+ }
+ else
+ {
+ numApplicationsFound--;
+ }
OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs
{
@@ -508,15 +604,6 @@ namespace Ryujinx.UI.App.Common
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");
@@ -554,10 +641,29 @@ namespace Ryujinx.UI.App.Common
return appMetadata;
}
- public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage)
+ public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong applicationId)
{
byte[] applicationIcon = null;
+ if (applicationId == 0)
+ {
+ if (Directory.Exists(applicationPath))
+ {
+ return _ncaIcon;
+ }
+
+ return Path.GetExtension(applicationPath).ToLower() switch
+ {
+ ".nsp" => _nspIcon,
+ ".pfs0" => _nspIcon,
+ ".xci" => _xciIcon,
+ ".nso" => _nsoIcon,
+ ".nro" => _nroIcon,
+ ".nca" => _ncaIcon,
+ _ => _ncaIcon,
+ };
+ }
+
try
{
// Look for icon only if applicationPath is not a directory
@@ -603,7 +709,16 @@ namespace Ryujinx.UI.App.Common
else
{
// Store the ControlFS in variable called controlFs
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
+ Dictionary<ulong, ContentMetaData> programs = pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel);
+ IFileSystem controlFs = null;
+
+ if (programs.TryGetValue(applicationId, out ContentMetaData value))
+ {
+ if (value.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca)
+ {
+ controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ }
+ }
// Read the icon from the ControlFS and store it as a byte array
try
@@ -630,16 +745,11 @@ namespace Ryujinx.UI.App.Common
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- using (MemoryStream stream = new())
- {
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
+ using MemoryStream stream = new();
+ icon.Get.AsStream().CopyTo(stream);
+ applicationIcon = stream.ToArray();
- if (applicationIcon != null)
- {
- break;
- }
+ break;
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
@@ -722,80 +832,79 @@ namespace Ryujinx.UI.App.Common
return applicationIcon ?? _ncaIcon;
}
- private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
+ private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data)
{
_ = 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();
+ data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
+ data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
}
else
{
- titleName = null;
- publisher = null;
+ data.Name = null;
+ data.Developer = null;
}
- if (string.IsNullOrWhiteSpace(titleName))
+ if (string.IsNullOrWhiteSpace(data.Name))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.NameString.IsEmpty())
{
- titleName = controlTitle.NameString.ToString();
+ data.Name = controlTitle.NameString.ToString();
break;
}
}
}
- if (string.IsNullOrWhiteSpace(publisher))
+ if (string.IsNullOrWhiteSpace(data.Developer))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.PublisherString.IsEmpty())
{
- publisher = controlTitle.PublisherString.ToString();
+ data.Developer = controlTitle.PublisherString.ToString();
break;
}
}
}
- if (controlData.PresenceGroupId != 0)
+ if (data.Id == 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";
+ if (controlData.SaveDataOwnerId != 0)
+ {
+ data.Id = controlData.SaveDataOwnerId;
+ }
+ else if (controlData.PresenceGroupId != 0)
+ {
+ data.Id = controlData.PresenceGroupId;
+ }
+ else if (controlData.AddOnContentBaseId != 0)
+ {
+ data.Id = (controlData.AddOnContentBaseId - 0x1000);
+ }
}
- version = controlData.DisplayVersionString.ToString();
+ data.Version = controlData.DisplayVersionString.ToString();
}
- private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
+ private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs)
{
updatedControlFs = null;
- string updatePath = "(unknown)";
+ string updatePath = null;
try
{
- (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
+ (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath);
if (patchNca != null && controlNca != null)
{
- updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
return true;
}
@@ -811,120 +920,5 @@ namespace Ryujinx.UI.App.Common
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);
- }
}
}
diff --git a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
index bbacd5fe..ae0e4d90 100644
--- a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
+++ b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
@@ -14,6 +14,7 @@ namespace Ryujinx.UI.Common.Helper
public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
+ public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static void ParseArguments(string[] args)
@@ -72,6 +73,10 @@ namespace Ryujinx.UI.Common.Helper
OverrideGraphicsBackend = args[++i];
break;
+ case "-i":
+ case "--application-id":
+ LaunchApplicationId = args[++i];
+ break;
case "--docked-mode":
OverrideDockedMode = true;
break;
diff --git a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
index c2085b28..58bdc90e 100644
--- a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
+++ b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
@@ -15,7 +15,7 @@ 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)
+ private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico";
@@ -25,13 +25,13 @@ namespace Ryujinx.UI.Common.Helper
image.Mutate(x => x.Resize(128, 128));
SaveBitmapAsIcon(image, iconPath);
- var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0);
+ var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), 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)
+ private static void CreateShortcutLinux(string applicationFilePath, string applicationId, 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");
@@ -41,11 +41,11 @@ namespace Ryujinx.UI.Common.Helper
image.SaveAsPng(iconPath);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
- outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}");
+ outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}");
}
[SupportedOSPlatform("macos")]
- private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
+ private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist");
@@ -64,7 +64,7 @@ namespace Ryujinx.UI.Common.Helper
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath);
- scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath));
+ scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
// Set execute permission
FileInfo fileInfo = new(scriptPath);
@@ -95,7 +95,7 @@ namespace Ryujinx.UI.Common.Helper
{
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
- CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
+ CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
return;
}
@@ -105,14 +105,14 @@ namespace Ryujinx.UI.Common.Helper
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);
+ CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
return;
}
if (OperatingSystem.IsMacOS())
{
- CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
+ CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
return;
}
@@ -120,7 +120,7 @@ namespace Ryujinx.UI.Common.Helper
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
}
- private static string GetArgsString(string appFilePath)
+ private static string GetArgsString(string appFilePath, string applicationId)
{
// args are first defined as a list, for easier adjustments in the future
var argsList = new List<string>();
@@ -131,6 +131,12 @@ namespace Ryujinx.UI.Common.Helper
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
+ if (appFilePath.ToLower().EndsWith(".xci"))
+ {
+ argsList.Add("--application-id");
+ argsList.Add($"\"{applicationId}\"");
+ }
+
argsList.Add($"\"{appFilePath}\"");
return String.Join(" ", argsList);