diff options
author | gdkchan <gab.dark.100@gmail.com> | 2023-11-11 23:35:30 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-11 23:35:30 -0300 |
commit | 51065d91290e41a9d2518f44c9bdf83a9b0017ab (patch) | |
tree | 4964520c8d5dbb1000b8eec4a024744df1a3e4ee /src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs | |
parent | 6228331fd1fb63a32d929bf1cae7f709bc9fd271 (diff) |
Revert "Add support for multi game XCIs (#5638)" (#5914)1.1.1079
This reverts commit 5c3cfb84c09b0566da677425915afa0b2d76da55.
Diffstat (limited to 'src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs')
-rw-r--r-- | src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs | 812 |
1 files changed, 412 insertions, 400 deletions
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 97612971..46f29851 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,18 +14,17 @@ 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,16 +42,15 @@ 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, IntegrityCheckLevel checkLevel) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) { _virtualFileSystem = virtualFileSystem; - _checkLevel = checkLevel; _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); @@ -71,241 +69,258 @@ namespace Ryujinx.Ui.App.Common return resourceByteArray; } - private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) + public void CancelLoading() { - ApplicationData data = new() - { - Icon = _nspIcon, - }; - - using UniqueRef<IFile> npdmFile = new(); - - try - { - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - data.Name = npdm.TitleName; - data.Id = npdm.Aci0.TitleId; - } + _cancellationToken?.Cancel(); + } - return data; - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}"); + public static void ReadControlData(IFileSystem controlFs, Span<byte> outProperty) + { + using UniqueRef<IFile> controlFile = new(); - return null; - } + controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); } - private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) + public void LoadApplications(List<string> appDirs, Language desiredTitleLanguage) { - bool isExeFs = false; + int numApplicationsFound = 0; + int numApplicationsLoaded = 0; + + _desiredTitleLanguage = desiredTitleLanguage; + + _cancellationToken = new CancellationTokenSource(); - // 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; + // Builds the applications list with paths to found applications + List<string> applications = new(); - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + try { - if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") + foreach (string appDir in appDirs) { - using UniqueRef<IFile> ncaFile = new(); + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + 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())) + try { - hasMainNca = true; + 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); + }); - break; + 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}\""); } } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - if (hasMainNca) - { - List<ApplicationData> applications = GetApplicationsFromPfs(pfs, filePath); - - switch (applications.Count) + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applications) { - 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; - } - } + if (_cancellationToken.Token.IsCancellationRequested) + { + return; + } - if (isExeFs) - { - return GetApplicationFromExeFs(pfs, filePath); - } + long fileSize = new FileInfo(applicationPath).Length; + string titleName = "Unknown"; + string titleId = "0000000000000000"; + string developer = "Unknown"; + string version = "0"; + byte[] applicationIcon = null; - return null; - } + BlitStruct<ApplicationControlProperty> controlHolder = new(1); - private List<ApplicationData> GetApplicationsFromPfs(IFileSystem pfs, string filePath) - { - var applications = new List<ApplicationData>(); - string extension = Path.GetExtension(filePath).ToLower(); + try + { + string extension = Path.GetExtension(applicationPath).ToLower(); - foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) - { - ApplicationData applicationData = new() - { - Id = titleId, - }; + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - try - { - Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); - Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + { + try + { + IFileSystem pfs; - BlitStruct<ApplicationControlProperty> controlHolder = new(1); + bool isExeFs = false; - IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + if (extension == ".xci") + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - // Check if there is an update available. - if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; - ReadControlData(controlFs, controlHolder.ByteSpan); + // 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; - GetApplicationInformation(ref controlHolder.Value, ref applicationData); + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + using UniqueRef<IFile> ncaFile = new(); - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef<IFile> icon = new(); + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - using MemoryStream stream = new(); + // 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; - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } - using var icon = new UniqueRef<IFile>(); + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + continue; + } + } - using MemoryStream stream = new(); + if (isExeFs) + { + applicationIcon = _nspIcon; - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); + using UniqueRef<IFile> npdmFile = new(); - if (applicationData.Icon != null) - { - break; - } - } + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); - applicationData.ControlHolder = controlHolder; + titleName = npdm.TitleName; + titleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); - applications.Add(applicationData); - } - catch (MissingKeyException exception) - { - applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; + // Check if there is an update available. + if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } - 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; + ReadControlData(controlFs, controlHolder.ByteSpan); - 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}"); - } - } + GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); - return applications; - } + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef<IFile> icon = new(); - private bool TryGetApplicationsFromFile(string applicationPath, out List<ApplicationData> applications) - { - applications = new List<ApplicationData>(); + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - long fileSizeBytes = new FileInfo(applicationPath).Length; + using MemoryStream stream = new(); - double fileSize = fileSizeBytes * 0.000000000931; + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } - BlitStruct<ApplicationControlProperty> controlHolder = new(1); + using var icon = new UniqueRef<IFile>(); - try - { - string extension = Path.GetExtension(applicationPath).ToLower(); + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + using MemoryStream stream = new(); - switch (extension) - { - case ".xci": - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); - applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); + if (applicationIcon != null) + { + break; + } + } - if (applications.Count == 0) + applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + } + } + catch (MissingKeyException exception) { - return false; + 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; - break; - } - case ".nsp": - case ".pfs0": - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).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: {applicationPath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); - ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); + numApplicationsFound--; - if (result == null) - { - return false; + continue; + } } - - applications.Add(result); - - break; - case ".nro": + else if (extension == ".nro") { BinaryReader reader = new(file); - ApplicationData application = new(); byte[] Read(long position, int size) { @@ -333,54 +348,46 @@ namespace Ryujinx.Ui.App.Common // Reads and stores game icon as byte array if (iconSize > 0) { - application.Icon = Read(assetOffset + iconOffset, (int)iconSize); + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); } else { - application.Icon = _nroIcon; + applicationIcon = _nroIcon; } // Read the NACP data Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - GetApplicationInformation(ref controlHolder.Value, ref application); + GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); } else { - application.Icon = _nroIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); + applicationIcon = _nroIcon; + titleName = 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}"); - return false; - } + numApplicationsFound--; - break; + continue; + } } - case ".nca": + else if (extension == ".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.IsProgram() || nca.IsPatch()) + if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) { - return false; - } + numApplicationsFound--; - application.Icon = _ncaIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); - application.ControlHolder = controlHolder; - - applications.Add(application); + continue; + } } catch (InvalidDataException) { @@ -390,178 +397,78 @@ namespace Ryujinx.Ui.App.Common { Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - return false; + numApplicationsFound--; + + continue; } - break; + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); } - // If its an NSO we just set defaults - case ".nso": + // If its an NSO we just set defaults + else if (extension == ".nso") { - ApplicationData application = new() - { - Icon = _nsoIcon, - Name = Path.GetFileNameWithoutExtension(applicationPath), - }; - - applications.Add(application); - break; + applicationIcon = _nsoIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); } - } - } - 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)) - { - appMetadata.LastPlayed = lastPlayedOldParsed; - - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; - } - } - }); - - 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) + catch (IOException exception) { - return; - } + Logger.Warning?.Print(LogClass.Application, exception.Message); - if (!Directory.Exists(appDir)) - { - Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); + numApplicationsFound--; continue; } - try + ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => { - 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); - }); + appMetadata.Title = titleName; - foreach (string app in files) + // 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) { - 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; - applicationPaths.Add(fullPath); - numApplicationsFound++; - } + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - } - 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 applicationPaths) - { - if (_cancellationToken.Token.IsCancellationRequested) - { - return; - } - - if (TryGetApplicationsFromFile(applicationPath, out List<ApplicationData> applications)) - { - foreach (var application in applications) + // 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) { - OnApplicationAdded(new ApplicationAddedEventArgs + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) { - AppData = application, - }); - } + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } - if (applications.Count > 1) - { - numApplicationsFound += applications.Count - 1; } + }); - numApplicationsLoaded += applications.Count; - } - else + ApplicationData data = new() { - numApplicationsFound--; - } + Favorite = appMetadata.Favorite, + Icon = applicationIcon, + TitleName = titleName, + TitleId = titleId, + Developer = developer, + Version = version, + TimePlayed = appMetadata.TimePlayed, + LastPlayed = appMetadata.LastPlayed, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, + Path = applicationPath, + ControlHolder = controlHolder, + }; + + numApplicationsLoaded++; + + OnApplicationAdded(new ApplicationAddedEventArgs + { + AppData = data, + }); OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { @@ -593,6 +500,15 @@ 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"); @@ -630,29 +546,10 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) { 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 @@ -698,16 +595,7 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - 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); - } - } + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); // Read the icon from the ControlFS and store it as a byte array try @@ -734,11 +622,16 @@ 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(); + } - break; + if (applicationIcon != null) + { + break; + } } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -821,41 +714,41 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) + 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) { - data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - data.Name = null; - data.Developer = null; + titleName = null; + publisher = null; } - if (string.IsNullOrWhiteSpace(data.Name)) + if (string.IsNullOrWhiteSpace(titleName)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - data.Name = controlTitle.NameString.ToString(); + titleName = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(data.Developer)) + if (string.IsNullOrWhiteSpace(publisher)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - data.Developer = controlTitle.PublisherString.ToString(); + publisher = controlTitle.PublisherString.ToString(); break; } @@ -864,21 +757,25 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - data.Id = controlData.PresenceGroupId; + titleId = controlData.PresenceGroupId.ToString("x16"); } else if (controlData.SaveDataOwnerId != 0) { - data.Id = controlData.SaveDataOwnerId; + titleId = controlData.SaveDataOwnerId.ToString(); } else if (controlData.AddOnContentBaseId != 0) { - data.Id = (controlData.AddOnContentBaseId - 0x1000); + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; } - data.Version = controlData.DisplayVersionString.ToString(); + version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) { updatedControlFs = null; @@ -886,11 +783,11 @@ namespace Ryujinx.Ui.App.Common try { - (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); + (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -906,5 +803,120 @@ 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); + } } } |