| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- using JsonPrettyPrinterPlus;
- using LibHac;
- using LibHac.Fs;
- using LibHac.Fs.Shim;
- using LibHac.FsSystem;
- using LibHac.FsSystem.NcaUtils;
- using LibHac.Ncm;
- using LibHac.Spl;
- using Ryujinx.Common.Logging;
- using Ryujinx.Configuration.System;
- using Ryujinx.HLE.FileSystem;
- using Ryujinx.HLE.Loaders.Npdm;
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Text;
- using Utf8Json;
- using Utf8Json.Resolvers;
- using RightsId = LibHac.Fs.RightsId;
- namespace Ryujinx.Ui
- {
- public class ApplicationLibrary
- {
- public static event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
- public static event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
- private static readonly byte[] _nspIcon = GetResourceBytes("Ryujinx.Ui.assets.NSPIcon.png");
- private static readonly byte[] _xciIcon = GetResourceBytes("Ryujinx.Ui.assets.XCIIcon.png");
- private static readonly byte[] _ncaIcon = GetResourceBytes("Ryujinx.Ui.assets.NCAIcon.png");
- private static readonly byte[] _nroIcon = GetResourceBytes("Ryujinx.Ui.assets.NROIcon.png");
- private static readonly byte[] _nsoIcon = GetResourceBytes("Ryujinx.Ui.assets.NSOIcon.png");
- private static VirtualFileSystem _virtualFileSystem;
- private static Language _desiredTitleLanguage;
- private static bool _loadingError;
- public static void LoadApplications(List<string> appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage)
- {
- int numApplicationsFound = 0;
- int numApplicationsLoaded = 0;
- _loadingError = false;
- _virtualFileSystem = virtualFileSystem;
- _desiredTitleLanguage = desiredTitleLanguage;
- // Builds the applications list with paths to found applications
- List<string> applications = new List<string>();
- foreach (string appDir in appDirs)
- {
- if (!Directory.Exists(appDir))
- {
- Logger.PrintWarning(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\"");
- continue;
- }
- foreach (string app in Directory.GetFiles(appDir, "*.*", SearchOption.AllDirectories))
- {
- if ((Path.GetExtension(app).ToLower() == ".nsp") ||
- (Path.GetExtension(app).ToLower() == ".pfs0")||
- (Path.GetExtension(app).ToLower() == ".xci") ||
- (Path.GetExtension(app).ToLower() == ".nca") ||
- (Path.GetExtension(app).ToLower() == ".nro") ||
- (Path.GetExtension(app).ToLower() == ".nso"))
- {
- applications.Add(app);
- numApplicationsFound++;
- }
- }
- }
- // Loops through applications list, creating a struct and then firing an event containing the struct for each application
- foreach (string applicationPath in applications)
- {
- double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
- string titleName = "Unknown";
- string titleId = "0000000000000000";
- string developer = "Unknown";
- string version = "0";
- string saveDataPath = null;
- byte[] applicationIcon = null;
- using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
- {
- if ((Path.GetExtension(applicationPath).ToLower() == ".nsp") ||
- (Path.GetExtension(applicationPath).ToLower() == ".pfs0") ||
- (Path.GetExtension(applicationPath).ToLower() == ".xci"))
- {
- try
- {
- PartitionFileSystem pfs;
- bool isExeFs = false;
- if (Path.GetExtension(applicationPath).ToLower() == ".xci")
- {
- Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage());
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- pfs = new PartitionFileSystem(file.AsStorage());
- // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
- bool hasMainNca = false;
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
- {
- if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
- {
- pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
- Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
- if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection())
- {
- hasMainNca = true;
- break;
- }
- }
- else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
- {
- isExeFs = true;
- }
- }
- if (!hasMainNca && !isExeFs)
- {
- numApplicationsFound--;
-
- continue;
- }
- }
- if (isExeFs)
- {
- applicationIcon = _nspIcon;
- Result result = pfs.OpenFile(out IFile npdmFile, "/main.npdm", OpenMode.Read);
- if (result != ResultFs.PathNotFound)
- {
- Npdm npdm = new Npdm(npdmFile.AsStream());
- titleName = npdm.TitleName;
- titleId = npdm.Aci0.TitleId.ToString("x16");
- }
- }
- else
- {
- // Store the ControlFS in variable called controlFs
- IFileSystem controlFs = GetControlFs(pfs);
- // Creates NACP class from the NACP file
- controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
- Nacp controlData = new Nacp(controlNacpFile.AsStream());
- // Get the title name, title ID, developer name and version number from the NACP
- version = controlData.DisplayVersion;
- GetNameIdDeveloper(controlData, out titleName, out titleId, out developer);
- // Read the icon from the ControlFS and store it as a byte array
- try
- {
- controlFs.OpenFile(out IFile icon, $"/icon_{_desiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure();
- using (MemoryStream stream = new MemoryStream())
- {
- icon.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
- }
- catch (HorizonResultException)
- {
- foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
- {
- if (entry.Name == "control.nacp")
- {
- continue;
- }
- controlFs.OpenFile(out IFile icon, entry.FullPath, OpenMode.Read).ThrowIfFailure();
- using (MemoryStream stream = new MemoryStream())
- {
- icon.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
- if (applicationIcon != null)
- {
- break;
- }
- }
- if (applicationIcon == null)
- {
- applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon;
- }
- }
- }
- }
- catch (MissingKeyException exception)
- {
- applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon;
- Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
- }
- catch (InvalidDataException)
- {
- applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon;
- Logger.PrintWarning(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.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- Logger.PrintDebug(LogClass.Application, exception.ToString());
-
- numApplicationsFound--;
- _loadingError = true;
- continue;
- }
- }
- else if (Path.GetExtension(applicationPath).ToLower() == ".nro")
- {
- BinaryReader reader = new BinaryReader(file);
- byte[] Read(long position, int size)
- {
- file.Seek(position, SeekOrigin.Begin);
- return reader.ReadBytes(size);
- }
- try
- {
- file.Seek(24, SeekOrigin.Begin);
- int assetOffset = reader.ReadInt32();
- if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
- {
- byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
- long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
- long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
- ulong nacpOffset = reader.ReadUInt64();
- ulong nacpSize = reader.ReadUInt64();
- // Reads and stores game icon as byte array
- applicationIcon = Read(assetOffset + iconOffset, (int) iconSize);
- // Creates memory stream out of byte array which is the NACP
- using (MemoryStream stream = new MemoryStream(Read(assetOffset + (int) nacpOffset, (int) nacpSize)))
- {
- // Creates NACP class from the memory stream
- Nacp controlData = new Nacp(stream);
- // Get the title name, title ID, developer name and version number from the NACP
- version = controlData.DisplayVersion;
- GetNameIdDeveloper(controlData, out titleName, out titleId, out developer);
- }
- }
- else
- {
- applicationIcon = _nroIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
- }
- catch
- {
- Logger.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
- continue;
- }
- }
- else if (Path.GetExtension(applicationPath).ToLower() == ".nca")
- {
- try
- {
- Nca nca = new Nca(_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.Header.GetFsHeader(dataIndex).IsPatchSection())
- {
- numApplicationsFound--;
- continue;
- }
- }
- catch (InvalidDataException)
- {
- Logger.PrintWarning(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
- }
- catch
- {
- Logger.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
- numApplicationsFound--;
- _loadingError = true;
-
- continue;
- }
- applicationIcon = _ncaIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
- // If its an NSO we just set defaults
- else if (Path.GetExtension(applicationPath).ToLower() == ".nso")
- {
- applicationIcon = _nsoIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
- }
- ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId);
- if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNum))
- {
- SaveDataFilter filter = new SaveDataFilter();
- filter.SetUserId(new UserId(1, 0));
- filter.SetTitleId(new TitleId(titleIdNum));
- Result result = virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
- if (result.IsSuccess())
- {
- saveDataPath = Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}");
- }
- }
- ApplicationData data = new ApplicationData()
- {
- Favorite = appMetadata.Favorite,
- Icon = applicationIcon,
- TitleName = titleName,
- TitleId = titleId,
- Developer = developer,
- Version = version,
- TimePlayed = ConvertSecondsToReadableString(appMetadata.TimePlayed),
- LastPlayed = appMetadata.LastPlayed,
- FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
- FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
- Path = applicationPath,
- SaveDataPath = saveDataPath
- };
- numApplicationsLoaded++;
- OnApplicationAdded(new ApplicationAddedEventArgs()
- {
- AppData = data
- });
- OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs()
- {
- NumAppsFound = numApplicationsFound,
- NumAppsLoaded = numApplicationsLoaded
- });
- }
- OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs()
- {
- NumAppsFound = numApplicationsFound,
- NumAppsLoaded = numApplicationsLoaded
- });
- if (_loadingError)
- {
- Gtk.Application.Invoke(delegate
- {
- GtkDialog.CreateErrorDialog("One or more files encountered were not of a valid type, check logs for more info.");
- });
- }
- }
- protected static void OnApplicationAdded(ApplicationAddedEventArgs e)
- {
- ApplicationAdded?.Invoke(null, e);
- }
- protected static void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e)
- {
- ApplicationCountUpdated?.Invoke(null, e);
- }
- private static byte[] GetResourceBytes(string resourceName)
- {
- Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
- byte[] resourceByteArray = new byte[resourceStream.Length];
- resourceStream.Read(resourceByteArray);
- return resourceByteArray;
- }
- private static IFileSystem GetControlFs(PartitionFileSystem pfs)
- {
- Nca controlNca = null;
- // Add keys to key set if needed
- foreach (DirectoryEntryEx ticketEntry in pfs.EnumerateEntries("/", "*.tik"))
- {
- Result result = pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read);
- if (result.IsSuccess())
- {
- Ticket ticket = new Ticket(ticketFile.AsStream());
- _virtualFileSystem.KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_virtualFileSystem.KeySet)));
- }
- }
- // Find the Control NCA and store it in variable called controlNca
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
- Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage());
- if (nca.Header.ContentType == NcaContentType.Control)
- {
- controlNca = nca;
- }
- }
- // Return the ControlFS
- return controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
- }
- internal static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
- {
- string metadataFolder = Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "gui");
- string metadataFile = Path.Combine(metadataFolder, "metadata.json");
- IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase });
- ApplicationMetadata appMetadata;
- if (!File.Exists(metadataFile))
- {
- Directory.CreateDirectory(metadataFolder);
- appMetadata = new ApplicationMetadata
- {
- Favorite = false,
- TimePlayed = 0,
- LastPlayed = "Never"
- };
- byte[] data = JsonSerializer.Serialize(appMetadata, resolver);
- File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson());
- }
- using (Stream stream = File.OpenRead(metadataFile))
- {
- appMetadata = JsonSerializer.Deserialize<ApplicationMetadata>(stream, resolver);
- }
- if (modifyFunction != null)
- {
- modifyFunction(appMetadata);
- byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver);
- File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
- }
- return appMetadata;
- }
- private static string ConvertSecondsToReadableString(double seconds)
- {
- const int secondsPerMinute = 60;
- const int secondsPerHour = secondsPerMinute * 60;
- const int secondsPerDay = secondsPerHour * 24;
- string readableString;
- if (seconds < secondsPerMinute)
- {
- readableString = $"{seconds}s";
- }
- else if (seconds < secondsPerHour)
- {
- readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins";
- }
- else if (seconds < secondsPerDay)
- {
- readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs";
- }
- else
- {
- readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days";
- }
- return readableString;
- }
- private static void GetNameIdDeveloper(Nacp controlData, out string titleName, out string titleId, out string developer)
- {
- Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
- NacpDescription nacpDescription = controlData.Descriptions.ToList().Find(x => x.Language == desiredTitleLanguage);
- if (nacpDescription != null)
- {
- titleName = nacpDescription.Title;
- developer = nacpDescription.Developer;
- }
- else
- {
- titleName = null;
- developer = null;
- }
- if (string.IsNullOrWhiteSpace(titleName))
- {
- titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
- }
- if (string.IsNullOrWhiteSpace(developer))
- {
- developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer;
- }
- if (controlData.PresenceGroupId != 0)
- {
- titleId = controlData.PresenceGroupId.ToString("x16");
- }
- else if (controlData.SaveDataOwnerId != 0)
- {
- titleId = controlData.SaveDataOwnerId.ToString("x16");
- }
- else if (controlData.AddOnContentBaseId != 0)
- {
- titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
- }
- else
- {
- titleId = "0000000000000000";
- }
- }
- }
- }
|