diff options
author | TSRBerry <20988865+TSRBerry@users.noreply.github.com> | 2023-11-11 21:56:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-11 21:56:57 +0100 |
commit | 5c3cfb84c09b0566da677425915afa0b2d76da55 (patch) | |
tree | d53c683c3ed3e685bec5b16ca661755d8815f66e /src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs | |
parent | 55557525b16f8256d91f769e026874b5c70c3b2d (diff) |
Add support for multi game XCIs (#5638)1.1.1076
* 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
* Fix formatting issues
* 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
---------
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Diffstat (limited to 'src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs')
-rw-r--r-- | src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs | 812 |
1 files changed, 400 insertions, 412 deletions
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 46f29851..97612971 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,17 +14,18 @@ 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; @@ -42,15 +43,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"); @@ -69,258 +71,241 @@ 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 + private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) + { + bool isExeFs = false; + + // 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("/", "*")) { - foreach (string appDir in appDirs) + if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } + using UniqueRef<IFile> ncaFile = new(); - if (!Directory.Exists(appDir)) - { - Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - continue; - } + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - try + // 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())) { - 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(); + hasMainNca = true; - 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}\""); + break; } } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } - // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applications) + if (hasMainNca) + { + List<ApplicationData> applications = GetApplicationsFromPfs(pfs, filePath); + + switch (applications.Count) { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } + 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; + } + } - long fileSize = new FileInfo(applicationPath).Length; - string titleName = "Unknown"; - string titleId = "0000000000000000"; - string developer = "Unknown"; - string version = "0"; - byte[] applicationIcon = null; + if (isExeFs) + { + return GetApplicationFromExeFs(pfs, filePath); + } - BlitStruct<ApplicationControlProperty> controlHolder = new(1); + return null; + } - try - { - string extension = Path.GetExtension(applicationPath).ToLower(); + private List<ApplicationData> GetApplicationsFromPfs(IFileSystem pfs, string filePath) + { + var applications = new List<ApplicationData>(); + string extension = Path.GetExtension(filePath).ToLower(); - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) + { + ApplicationData applicationData = new() + { + Id = titleId, + }; - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") - { - try - { - IFileSystem pfs; + try + { + Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); - bool isExeFs = false; + BlitStruct<ApplicationControlProperty> controlHolder = new(1); - if (extension == ".xci") - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; + // Check if there is an update available. + if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } - // 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; + ReadControlData(controlFs, controlHolder.ByteSpan); - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") - { - using UniqueRef<IFile> ncaFile = new(); + GetApplicationInformation(ref controlHolder.Value, ref applicationData); - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef<IFile> icon = new(); - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - // 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; + using MemoryStream stream = new(); - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } - if (!hasMainNca && !isExeFs) - { - numApplicationsFound--; + using var icon = new UniqueRef<IFile>(); - continue; - } - } + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - if (isExeFs) - { - applicationIcon = _nspIcon; + using MemoryStream stream = new(); - using UniqueRef<IFile> npdmFile = new(); + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + if (applicationData.Icon != null) + { + break; + } + } - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); + applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } - titleName = npdm.TitleName; - titleId = npdm.Aci0.TitleId.ToString("x16"); - } - } - else - { - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); + applicationData.ControlHolder = controlHolder; - // Check if there is an update available. - if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } + applications.Add(applicationData); + } + catch (MissingKeyException exception) + { + applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; - ReadControlData(controlFs, controlHolder.ByteSpan); + 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; - GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); + 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}"); + } + } - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef<IFile> icon = new(); + return applications; + } - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + private bool TryGetApplicationsFromFile(string applicationPath, out List<ApplicationData> applications) + { + applications = new List<ApplicationData>(); - using MemoryStream stream = new(); + long fileSizeBytes = new FileInfo(applicationPath).Length; - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } + double fileSize = fileSizeBytes * 0.000000000931; - 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; } - else if (extension == ".nro") + + applications.Add(result); + + break; + case ".nro": { BinaryReader reader = new(file); + ApplicationData application = new(); byte[] Read(long position, int size) { @@ -348,46 +333,54 @@ 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--; - - continue; + return false; } + + break; } - 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) { @@ -397,78 +390,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 - else if (extension == ".nso") + // If its an NSO we just set defaults + case ".nso": { - applicationIcon = _nsoIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); + ApplicationData application = new() + { + Icon = _nsoIcon, + Name = Path.GetFileNameWithoutExtension(applicationPath), + }; + + applications.Add(application); + break; } + } + } + 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; } - catch (IOException exception) + + // 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) { - Logger.Warning?.Print(LogClass.Application, exception.Message); + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; - numApplicationsFound--; + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } - continue; + } + }); + + data.Favorite = appMetadata.Favorite; + data.TimePlayed = appMetadata.TimePlayed; + data.LastPlayed = appMetadata.LastPlayed; + data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); + data.FileSize = new FileInfo(applicationPath).Length; + 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) + { + return; } - ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => + if (!Directory.Exists(appDir)) { - appMetadata.Title = titleName; + Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); - // 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) + continue; + } + + try + { + 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 { @@ -500,15 +593,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"); @@ -546,10 +630,29 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) { byte[] applicationIcon = null; + if (titleId == 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 @@ -595,7 +698,16 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); + Dictionary<ulong, ContentCollection> programs = pfs.GetApplicationData(_virtualFileSystem, _checkLevel); + IFileSystem controlFs = null; + + if (programs.ContainsKey(titleId)) + { + if (programs[titleId].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 @@ -622,16 +734,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; @@ -714,41 +821,41 @@ 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; } @@ -757,25 +864,21 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - titleId = controlData.PresenceGroupId.ToString("x16"); + data.Id = controlData.PresenceGroupId; } else if (controlData.SaveDataOwnerId != 0) { - titleId = controlData.SaveDataOwnerId.ToString(); + data.Id = controlData.SaveDataOwnerId; } else if (controlData.AddOnContentBaseId != 0) { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - else - { - titleId = "0000000000000000"; + 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; @@ -783,11 +886,11 @@ namespace Ryujinx.Ui.App.Common 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; } @@ -803,120 +906,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); - } } } |