2024-08-17 03:26:25 +00:00
using DynamicData ;
2022-05-15 11:30:15 +00:00
using LibHac ;
using LibHac.Common ;
using LibHac.Fs ;
using LibHac.Fs.Fsa ;
using LibHac.FsSystem ;
2024-07-16 21:17:32 +00:00
using LibHac.Ncm ;
2022-05-15 11:30:15 +00:00
using LibHac.Ns ;
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
using LibHac.Tools.FsSystem.NcaUtils ;
using Ryujinx.Common.Configuration ;
using Ryujinx.Common.Logging ;
2023-04-03 10:14:19 +00:00
using Ryujinx.Common.Utilities ;
2022-05-15 11:30:15 +00:00
using Ryujinx.HLE.FileSystem ;
using Ryujinx.HLE.HOS.SystemState ;
using Ryujinx.HLE.Loaders.Npdm ;
2024-07-16 21:17:32 +00:00
using Ryujinx.HLE.Loaders.Processes.Extensions ;
2024-08-15 01:54:32 +00:00
using Ryujinx.HLE.Utilities ;
2024-02-11 02:09:18 +00:00
using Ryujinx.UI.Common.Configuration ;
using Ryujinx.UI.Common.Configuration.System ;
2024-08-17 03:26:25 +00:00
using Ryujinx.UI.Common.Helper ;
2024-08-16 02:17:37 +00:00
using Ryujinx.UI.Common.Models ;
2022-05-15 11:30:15 +00:00
using System ;
using System.Collections.Generic ;
2024-08-17 03:26:25 +00:00
using System.Collections.ObjectModel ;
2022-05-15 11:30:15 +00:00
using System.IO ;
2023-06-27 23:18:19 +00:00
using System.Linq ;
2022-05-15 11:30:15 +00:00
using System.Reflection ;
using System.Text ;
using System.Text.Json ;
using System.Threading ;
2024-07-16 21:17:32 +00:00
using ContentType = LibHac . Ncm . ContentType ;
2024-08-17 03:26:25 +00:00
using MissingKeyException = LibHac . Common . Keys . MissingKeyException ;
2022-05-15 11:30:15 +00:00
using Path = System . IO . Path ;
2024-08-16 02:17:37 +00:00
using SpanHelpers = LibHac . Common . SpanHelpers ;
2023-06-29 00:39:22 +00:00
using TimeSpan = System . TimeSpan ;
2022-05-15 11:30:15 +00:00
2024-02-11 02:09:18 +00:00
namespace Ryujinx.UI.App.Common
2022-05-15 11:30:15 +00:00
{
public class ApplicationLibrary
{
2024-08-03 21:31:34 +00:00
public Language DesiredLanguage { get ; set ; }
2023-06-29 00:39:22 +00:00
public event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
2022-05-15 11:30:15 +00:00
public event EventHandler < ApplicationCountUpdatedEventArgs > ApplicationCountUpdated ;
2024-08-15 01:54:32 +00:00
public event EventHandler < TitleUpdateAddedEventArgs > TitleUpdateAdded ;
public event EventHandler < DownloadableContentAddedEventArgs > DownloadableContentAdded ;
2022-05-15 11:30:15 +00:00
2024-08-17 03:26:25 +00:00
public IObservableCache < ApplicationData , string > Applications ;
public IObservableCache < ( TitleUpdateModel TitleUpdate , bool IsSelected ) , TitleUpdateModel > TitleUpdates ;
public IObservableCache < ( DownloadableContentModel Dlc , bool IsEnabled ) , DownloadableContentModel > DownloadableContents ;
2022-05-15 11:30:15 +00:00
private readonly byte [ ] _nspIcon ;
private readonly byte [ ] _xciIcon ;
private readonly byte [ ] _ncaIcon ;
private readonly byte [ ] _nroIcon ;
private readonly byte [ ] _nsoIcon ;
2023-01-15 23:11:16 +00:00
private readonly VirtualFileSystem _virtualFileSystem ;
2024-07-16 21:17:32 +00:00
private readonly IntegrityCheckLevel _checkLevel ;
2023-06-29 00:39:22 +00:00
private CancellationTokenSource _cancellationToken ;
2024-08-17 03:26:25 +00:00
private readonly SourceCache < ApplicationData , string > _applications = new ( it = > it . Path ) ;
private readonly SourceCache < ( TitleUpdateModel TitleUpdate , bool IsSelected ) , TitleUpdateModel > _titleUpdates = new ( it = > it . TitleUpdate ) ;
private readonly SourceCache < ( DownloadableContentModel Dlc , bool IsEnabled ) , DownloadableContentModel > _downloadableContents = new ( it = > it . Dlc ) ;
2022-05-15 11:30:15 +00:00
2023-06-29 00:39:22 +00:00
private static readonly ApplicationJsonSerializerContext _serializerContext = new ( JsonHelper . GetDefaultSerializerOptions ( ) ) ;
2023-04-03 10:14:19 +00:00
2024-07-16 21:17:32 +00:00
public ApplicationLibrary ( VirtualFileSystem virtualFileSystem , IntegrityCheckLevel checkLevel )
2022-05-15 11:30:15 +00:00
{
_virtualFileSystem = virtualFileSystem ;
2024-07-16 21:17:32 +00:00
_checkLevel = checkLevel ;
2022-05-15 11:30:15 +00:00
2024-08-17 03:26:25 +00:00
Applications = _applications . AsObservableCache ( ) ;
TitleUpdates = _titleUpdates . AsObservableCache ( ) ;
DownloadableContents = _downloadableContents . AsObservableCache ( ) ;
2024-02-11 02:09:18 +00:00
_nspIcon = GetResourceBytes ( "Ryujinx.UI.Common.Resources.Icon_NSP.png" ) ;
_xciIcon = GetResourceBytes ( "Ryujinx.UI.Common.Resources.Icon_XCI.png" ) ;
_ncaIcon = GetResourceBytes ( "Ryujinx.UI.Common.Resources.Icon_NCA.png" ) ;
_nroIcon = GetResourceBytes ( "Ryujinx.UI.Common.Resources.Icon_NRO.png" ) ;
_nsoIcon = GetResourceBytes ( "Ryujinx.UI.Common.Resources.Icon_NSO.png" ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
private static byte [ ] GetResourceBytes ( string resourceName )
2022-05-15 11:30:15 +00:00
{
2023-06-29 00:39:22 +00:00
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
2022-05-15 11:30:15 +00:00
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
2024-06-02 20:16:48 +00:00
resourceStream . ReadExactly ( resourceByteArray ) ;
2022-05-15 11:30:15 +00:00
return resourceByteArray ;
}
2024-07-20 19:35:43 +00:00
/// <exception cref="Ryujinx.HLE.Exceptions.InvalidNpdmException">The npdm file doesn't contain valid data.</exception>
/// <exception cref="NotImplementedException">The FsAccessHeader.ContentOwnerId section is not implemented.</exception>
/// <exception cref="ArgumentException">An error occured while reading bytes from the stream.</exception>
/// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
/// <exception cref="IOException">An I/O error occurred.</exception>
2024-07-16 21:17:32 +00:00
private ApplicationData GetApplicationFromExeFs ( PartitionFileSystem pfs , string filePath )
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
ApplicationData data = new ( )
{
Icon = _nspIcon ,
2024-07-20 19:35:43 +00:00
Path = filePath ,
2024-07-16 21:17:32 +00:00
} ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
using UniqueRef < IFile > npdmFile = new ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
Result result = pfs . OpenFile ( ref npdmFile . Ref , "/main.npdm" . ToU8Span ( ) , OpenMode . Read ) ;
2023-11-12 02:35:30 +00:00
2024-07-20 19:35:43 +00:00
if ( ResultFs . PathNotFound . Includes ( result ) )
2024-07-16 21:17:32 +00:00
{
2024-07-20 19:35:43 +00:00
Npdm npdm = new ( npdmFile . Get . AsStream ( ) ) ;
2023-11-11 20:56:57 +00:00
2024-07-20 19:35:43 +00:00
data . Name = npdm . TitleName ;
data . Id = npdm . Aci0 . TitleId ;
2024-07-16 21:17:32 +00:00
}
2024-07-20 19:35:43 +00:00
return data ;
2024-07-16 21:17:32 +00:00
}
2023-11-11 20:56:57 +00:00
2024-08-17 03:26:25 +00:00
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
2024-07-20 19:35:43 +00:00
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>
/// <exception cref="HorizonResultException">An error occured while reading PFS data.</exception>
/// <exception cref="Ryujinx.HLE.Exceptions.InvalidNpdmException">The npdm file doesn't contain valid data.</exception>
/// <exception cref="NotImplementedException">The FsAccessHeader.ContentOwnerId section is not implemented.</exception>
/// <exception cref="ArgumentException">An error occured while reading bytes from the stream.</exception>
/// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
/// <exception cref="IOException">An I/O error occurred.</exception>
2024-07-16 21:17:32 +00:00
private ApplicationData GetApplicationFromNsp ( PartitionFileSystem pfs , string filePath )
{
bool isExeFs = false ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
// 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 ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*" ) )
{
if ( Path . GetExtension ( fileEntry . FullPath ) ? . ToLower ( ) = = ".nca" )
{
using UniqueRef < IFile > ncaFile = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-11-12 02:35:30 +00:00
try
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
pfs . OpenFile ( ref ncaFile . Ref , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2023-03-31 19:16:46 +00:00
2024-07-16 21:17:32 +00:00
Nca nca = new ( _virtualFileSystem . KeySet , ncaFile . Get . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2023-12-04 18:16:34 +00:00
2024-07-16 21:17:32 +00:00
// 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 ;
2023-12-04 18:16:34 +00:00
2024-07-16 21:17:32 +00:00
break ;
2023-11-12 02:35:30 +00:00
}
}
2024-07-16 21:17:32 +00:00
catch ( Exception exception )
2023-11-12 02:35:30 +00:00
{
2024-07-16 21:17:32 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"Encountered an error while trying to load applications from file '{filePath}': {exception}" ) ;
return null ;
2022-12-29 15:52:30 +00:00
}
2022-05-15 11:30:15 +00:00
}
2024-07-16 21:17:32 +00:00
else if ( Path . GetFileNameWithoutExtension ( fileEntry . FullPath ) = = "main" )
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
isExeFs = true ;
}
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
if ( hasMainNca )
{
List < ApplicationData > applications = GetApplicationsFromPfs ( pfs , filePath ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
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 ;
}
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
if ( isExeFs )
{
return GetApplicationFromExeFs ( pfs , filePath ) ;
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
return null ;
}
2023-01-15 23:11:16 +00:00
2024-08-17 03:26:25 +00:00
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
2024-07-20 19:35:43 +00:00
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>
/// <exception cref="HorizonResultException">An error occured while reading PFS data.</exception>
2024-07-16 21:17:32 +00:00
private List < ApplicationData > GetApplicationsFromPfs ( IFileSystem pfs , string filePath )
{
var applications = new List < ApplicationData > ( ) ;
string extension = Path . GetExtension ( filePath ) . ToLower ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
foreach ( ( ulong titleId , ContentMetaData content ) in pfs . GetContentData ( ContentMetaType . Application , _virtualFileSystem , _checkLevel ) )
2024-07-16 21:17:32 +00:00
{
2024-07-20 19:35:43 +00:00
ApplicationData applicationData = new ( )
2024-07-16 21:17:32 +00:00
{
2024-07-20 19:35:43 +00:00
Id = titleId ,
Path = filePath ,
} ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
Nca mainNca = content . GetNcaByType ( _virtualFileSystem . KeySet , ContentType . Program ) ;
Nca controlNca = content . GetNcaByType ( _virtualFileSystem . KeySet , ContentType . Control ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
BlitStruct < ApplicationControlProperty > controlHolder = new ( 1 ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
IFileSystem controlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , _checkLevel ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
// Check if there is an update available.
if ( IsUpdateApplied ( mainNca , out IFileSystem updatedControlFs ) )
{
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs ;
}
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
if ( controlFs = = null )
{
continue ;
}
2024-07-17 22:02:20 +00:00
2024-07-20 19:35:43 +00:00
ReadControlData ( controlFs , controlHolder . ByteSpan ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
GetApplicationInformation ( ref controlHolder . Value , ref applicationData ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
// Read the icon from the ControlFS and store it as a byte array
try
{
using UniqueRef < IFile > icon = new ( ) ;
2023-01-15 23:11:16 +00:00
2024-08-03 21:31:34 +00:00
controlFs . OpenFile ( ref icon . Ref , $"/icon_{DesiredLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
using MemoryStream stream = new ( ) ;
2023-01-15 23:11:16 +00:00
2024-07-20 19:35:43 +00:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationData . Icon = stream . ToArray ( ) ;
}
catch ( HorizonResultException )
{
foreach ( DirectoryEntryEx entry in controlFs . EnumerateEntries ( "/" , "*" ) )
2024-07-16 21:17:32 +00:00
{
2024-07-20 19:35:43 +00:00
if ( entry . Name = = "control.nacp" )
2024-07-16 21:17:32 +00:00
{
2024-07-20 19:35:43 +00:00
continue ;
}
2023-01-15 23:11:16 +00:00
2024-07-20 19:35:43 +00:00
using var icon = new UniqueRef < IFile > ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
controlFs . OpenFile ( ref icon . Ref , entry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
using MemoryStream stream = new ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationData . Icon = stream . ToArray ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
if ( applicationData . Icon ! = null )
{
break ;
2024-07-16 21:17:32 +00:00
}
}
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
applicationData . Icon ? ? = extension = = ".xci" ? _xciIcon : _nspIcon ;
2024-07-16 21:17:32 +00:00
}
2024-07-20 19:35:43 +00:00
applicationData . ControlHolder = controlHolder ;
applications . Add ( applicationData ) ;
2024-07-16 21:17:32 +00:00
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
return applications ;
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
public bool TryGetApplicationsFromFile ( string applicationPath , out List < ApplicationData > applications )
{
applications = [ ] ;
2024-08-03 17:46:59 +00:00
long fileSize ;
2024-07-16 21:17:32 +00:00
2024-08-03 17:46:59 +00:00
try
{
fileSize = new FileInfo ( applicationPath ) . Length ;
}
catch ( FileNotFoundException )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file was not found: '{applicationPath}'" ) ;
2024-08-17 03:28:30 +00:00
2024-08-03 17:46:59 +00:00
return false ;
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
BlitStruct < ApplicationControlProperty > controlHolder = new ( 1 ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
try
{
string extension = Path . GetExtension ( applicationPath ) . ToLower ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
using FileStream file = new ( applicationPath , FileMode . Open , FileAccess . Read ) ;
2023-03-31 19:16:46 +00:00
2024-07-16 21:17:32 +00:00
switch ( extension )
{
case ".xci" :
{
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
applications = GetApplicationsFromPfs ( xci . OpenPartition ( XciPartitionType . Secure ) , applicationPath ) ;
2023-01-15 23:11:16 +00:00
2024-07-16 21:17:32 +00:00
if ( applications . Count = = 0 )
2023-01-15 23:11:16 +00:00
{
2024-07-16 21:17:32 +00:00
return false ;
2023-01-15 23:11:16 +00:00
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
break ;
}
case ".nsp" :
case ".pfs0" :
{
var pfs = new PartitionFileSystem ( ) ;
pfs . Initialize ( file . AsStorage ( ) ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
ApplicationData result = GetApplicationFromNsp ( pfs , applicationPath ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
if ( result = = null )
{
return false ;
2023-11-12 02:35:30 +00:00
}
2024-07-16 21:17:32 +00:00
applications . Add ( result ) ;
break ;
2023-01-15 23:11:16 +00:00
}
2024-07-16 21:17:32 +00:00
case ".nro" :
2023-01-15 23:11:16 +00:00
{
BinaryReader reader = new ( file ) ;
2024-07-16 21:17:32 +00:00
ApplicationData application = new ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
file . Seek ( 24 , SeekOrigin . Begin ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
int assetOffset = reader . ReadInt32 ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
if ( Encoding . ASCII . GetString ( Read ( assetOffset , 4 ) ) = = "ASET" )
{
byte [ ] iconSectionInfo = Read ( assetOffset + 8 , 0x10 ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
long iconOffset = BitConverter . ToInt64 ( iconSectionInfo , 0 ) ;
long iconSize = BitConverter . ToInt64 ( iconSectionInfo , 8 ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
ulong nacpOffset = reader . ReadUInt64 ( ) ;
ulong nacpSize = reader . ReadUInt64 ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-20 19:35:43 +00:00
// Reads and stores game icon as byte array
if ( iconSize > 0 )
{
application . Icon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
else
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
application . Icon = _nroIcon ;
2022-05-15 11:30:15 +00:00
}
2024-07-16 21:17:32 +00:00
2024-07-20 19:35:43 +00:00
// Read the NACP data
Read ( assetOffset + ( int ) nacpOffset , ( int ) nacpSize ) . AsSpan ( ) . CopyTo ( controlHolder . ByteSpan ) ;
GetApplicationInformation ( ref controlHolder . Value , ref application ) ;
2022-05-15 11:30:15 +00:00
}
2024-07-20 19:35:43 +00:00
else
2022-05-15 11:30:15 +00:00
{
2024-07-20 19:35:43 +00:00
application . Icon = _nroIcon ;
application . Name = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2024-07-16 21:17:32 +00:00
}
2023-11-11 20:56:57 +00:00
2024-07-20 19:35:43 +00:00
application . ControlHolder = controlHolder ;
applications . Add ( application ) ;
2024-07-16 21:17:32 +00:00
break ;
byte [ ] Read ( long position , int size )
{
file . Seek ( position , SeekOrigin . Begin ) ;
return reader . ReadBytes ( size ) ;
2023-11-12 02:35:30 +00:00
}
2023-01-15 23:11:16 +00:00
}
2024-07-16 21:17:32 +00:00
case ".nca" :
2023-01-15 23:11:16 +00:00
{
2024-07-20 19:35:43 +00:00
ApplicationData application = new ( ) ;
2024-07-16 21:17:32 +00:00
2024-07-20 19:35:43 +00:00
Nca nca = new ( _virtualFileSystem . KeySet , new FileStream ( applicationPath , FileMode . Open , FileAccess . Read ) . AsStorage ( ) ) ;
2024-07-16 21:17:32 +00:00
2024-07-20 19:35:43 +00:00
if ( ! nca . IsProgram ( ) | | nca . IsPatch ( ) )
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
return false ;
2023-01-15 23:11:16 +00:00
}
2024-07-20 19:35:43 +00:00
application . Icon = _ncaIcon ;
application . Name = Path . GetFileNameWithoutExtension ( applicationPath ) ;
application . ControlHolder = controlHolder ;
applications . Add ( application ) ;
2024-07-16 21:17:32 +00:00
break ;
}
// If its an NSO we just set defaults
case ".nso" :
{
ApplicationData application = new ( )
{
Icon = _nsoIcon ,
Name = Path . GetFileNameWithoutExtension ( applicationPath ) ,
} ;
applications . Add ( application ) ;
2024-07-20 19:35:43 +00:00
2024-07-16 21:17:32 +00:00
break ;
2023-01-15 23:11:16 +00:00
}
2024-07-16 21:17:32 +00:00
}
}
2024-07-20 19:35:43 +00:00
catch ( MissingKeyException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
return false ;
}
catch ( InvalidDataException )
{
Logger . Warning ? . Print ( LogClass . Application , $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}" ) ;
return false ;
}
2024-07-16 21:17:32 +00:00
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application , exception . Message ) ;
return false ;
}
2024-07-20 19:35:43 +00:00
catch ( Exception exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}" ) ;
return false ;
}
2024-07-16 21:17:32 +00:00
foreach ( var data in applications )
{
2024-08-03 21:31:34 +00:00
// Only load metadata for applications with an ID
if ( data . Id ! = 0 )
2024-07-16 21:17:32 +00:00
{
2024-08-03 21:31:34 +00:00
ApplicationMetadata appMetadata = LoadAndSaveMetaData ( data . IdString , appMetadata = >
2024-07-16 21:17:32 +00:00
{
2024-08-03 21:31:34 +00:00
appMetadata . Title = data . Name ;
2024-07-16 21:17:32 +00:00
2024-08-03 21:31:34 +00:00
// 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 )
2023-01-15 23:11:16 +00:00
{
2024-08-03 21:31:34 +00:00
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 ;
}
2024-07-16 21:17:32 +00:00
2022-05-15 11:30:15 +00:00
}
2024-08-03 21:31:34 +00:00
} ) ;
2024-07-16 21:17:32 +00:00
2024-08-03 21:31:34 +00:00
data . Favorite = appMetadata . Favorite ;
data . TimePlayed = appMetadata . TimePlayed ;
data . LastPlayed = appMetadata . LastPlayed ;
}
2024-07-16 21:17:32 +00:00
data . FileExtension = Path . GetExtension ( applicationPath ) . TrimStart ( '.' ) . ToUpper ( ) ;
data . FileSize = fileSize ;
data . Path = applicationPath ;
}
return true ;
}
2024-08-17 03:28:30 +00:00
2024-08-16 02:17:37 +00:00
public bool TryGetDownloadableContentFromFile ( string filePath , out List < DownloadableContentModel > titleUpdates )
2024-08-15 01:54:32 +00:00
{
titleUpdates = [ ] ;
try
{
string extension = Path . GetExtension ( filePath ) . ToLower ( ) ;
using FileStream file = new ( filePath , FileMode . Open , FileAccess . Read ) ;
switch ( extension )
{
case ".xci" :
case ".nsp" :
{
IntegrityCheckLevel checkLevel = ConfigurationState . Instance . System . EnableFsIntegrityChecks
? IntegrityCheckLevel . ErrorOnInvalid
: IntegrityCheckLevel . None ;
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
using IFileSystem pfs = PartitionFileSystemUtils . OpenApplicationFileSystem ( filePath , _virtualFileSystem ) ;
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
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 = TryOpenNca ( ncaFile . Get . AsStorage ( ) ) ;
if ( nca = = null )
{
continue ;
}
if ( nca . Header . ContentType = = NcaContentType . PublicData )
{
2024-08-16 03:41:12 +00:00
titleUpdates . Add ( new DownloadableContentModel ( nca . Header . TitleId , filePath , fileEntry . FullPath ) ) ;
2024-08-15 01:54:32 +00:00
}
}
2024-08-16 01:47:04 +00:00
return titleUpdates . Count ! = 0 ;
2024-08-15 01:54:32 +00:00
}
}
}
catch ( MissingKeyException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
}
catch ( InvalidDataException )
{
Logger . Warning ? . Print ( LogClass . Application , $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}" ) ;
}
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application , exception . Message ) ;
}
catch ( Exception exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}" ) ;
}
return false ;
}
2024-08-17 03:28:30 +00:00
2024-08-16 02:17:37 +00:00
public bool TryGetTitleUpdatesFromFile ( string filePath , out List < TitleUpdateModel > titleUpdates )
2024-08-15 01:54:32 +00:00
{
titleUpdates = [ ] ;
try
{
string extension = Path . GetExtension ( filePath ) . ToLower ( ) ;
using FileStream file = new ( filePath , FileMode . Open , FileAccess . Read ) ;
switch ( extension )
{
case ".xci" :
case ".nsp" :
{
IntegrityCheckLevel checkLevel = ConfigurationState . Instance . System . EnableFsIntegrityChecks
? IntegrityCheckLevel . ErrorOnInvalid
: IntegrityCheckLevel . None ;
2024-08-16 02:17:37 +00:00
using IFileSystem pfs =
PartitionFileSystemUtils . OpenApplicationFileSystem ( filePath , _virtualFileSystem ) ;
Dictionary < ulong , ContentMetaData > updates =
pfs . GetContentData ( ContentMetaType . Patch , _virtualFileSystem , checkLevel ) ;
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
if ( updates . Count = = 0 )
{
return false ;
}
2024-08-16 02:17:37 +00:00
foreach ( ( _ , ContentMetaData content ) in updates )
{
Nca patchNca = content . GetNcaByType ( _virtualFileSystem . KeySet , ContentType . Program ) ;
Nca controlNca = content . GetNcaByType ( _virtualFileSystem . KeySet , ContentType . Control ) ;
if ( controlNca ! = null & & patchNca ! = null )
{
ApplicationControlProperty controlData = new ( ) ;
using UniqueRef < IFile > nacpFile = new ( ) ;
controlNca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None )
. OpenFile ( ref nacpFile . Ref , "/control.nacp" . ToU8Span ( ) , OpenMode . Read )
. ThrowIfFailure ( ) ;
nacpFile . Get . Read ( out _ , 0 , SpanHelpers . AsByteSpan ( ref controlData ) ,
ReadOption . None ) . ThrowIfFailure ( ) ;
2024-08-15 01:54:32 +00:00
2024-08-16 02:17:37 +00:00
var displayVersion = controlData . DisplayVersionString . ToString ( ) ;
var update = new TitleUpdateModel ( content . ApplicationId , content . Version . Version ,
displayVersion , filePath ) ;
titleUpdates . Add ( update ) ;
}
}
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
return true ;
}
}
}
catch ( MissingKeyException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
}
catch ( InvalidDataException )
{
Logger . Warning ? . Print ( LogClass . Application , $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}" ) ;
}
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application , exception . Message ) ;
}
catch ( Exception exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}" ) ;
}
return false ;
}
2024-07-16 21:17:32 +00:00
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 ( ) ;
}
2024-08-03 21:31:34 +00:00
public void LoadApplications ( List < string > appDirs )
2024-07-16 21:17:32 +00:00
{
int numApplicationsFound = 0 ;
int numApplicationsLoaded = 0 ;
_cancellationToken = new CancellationTokenSource ( ) ;
2024-08-17 03:26:25 +00:00
_applications . Clear ( ) ;
2024-07-16 21:17:32 +00:00
// Builds the applications list with paths to found applications
List < string > applicationPaths = new ( ) ;
try
{
foreach ( string appDir in appDirs )
{
if ( _cancellationToken . Token . IsCancellationRequested )
2023-11-11 20:56:57 +00:00
{
2024-07-16 21:17:32 +00:00
return ;
}
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
if ( ! Directory . Exists ( appDir ) )
{
Logger . Warning ? . Print ( LogClass . Application , $"The specified game directory \" { appDir } \ " does not exist." ) ;
2022-05-15 11:30:15 +00:00
2023-11-11 20:56:57 +00:00
continue ;
}
2024-07-16 21:17:32 +00:00
try
2023-11-11 20:56:57 +00:00
{
2024-08-03 17:46:59 +00:00
EnumerationOptions options = new ( )
{
RecurseSubdirectories = true ,
IgnoreInaccessible = false ,
} ;
IEnumerable < string > files = Directory . EnumerateFiles ( appDir , "*" , options ) . Where ( file = >
2023-05-11 23:56:37 +00:00
{
2024-07-16 21:17:32 +00:00
return
2024-08-15 01:54:32 +00:00
( 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 ) ;
2024-07-16 21:17:32 +00:00
} ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
foreach ( string app in files )
2023-11-11 20:56:57 +00:00
{
2024-07-16 21:17:32 +00:00
if ( _cancellationToken . Token . IsCancellationRequested )
2023-11-11 20:56:57 +00:00
{
2024-07-16 21:17:32 +00:00
return ;
2023-11-12 02:35:30 +00:00
}
2023-11-11 20:56:57 +00:00
2024-07-16 21:17:32 +00:00
var fileInfo = new FileInfo ( app ) ;
2024-08-03 17:46:59 +00:00
try
2024-07-16 21:17:32 +00:00
{
var fullPath = fileInfo . ResolveLinkTarget ( true ) ? . FullName ? ? fileInfo . FullName ;
2024-08-03 17:46:59 +00:00
2024-07-16 21:17:32 +00:00
applicationPaths . Add ( fullPath ) ;
numApplicationsFound + + ;
}
2024-08-03 17:46:59 +00:00
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Failed to resolve the full path to file: \" { app } \ " Error: {exception}" ) ;
}
2023-11-11 20:56:57 +00:00
}
2024-07-16 21:17:32 +00:00
}
catch ( UnauthorizedAccessException )
{
Logger . Warning ? . Print ( LogClass . Application , $"Failed to get access to directory: \" { appDir } \ "" ) ;
}
}
2023-11-11 20:56:57 +00:00
2024-07-16 21:17:32 +00:00
// 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 )
2023-11-11 20:56:57 +00:00
{
2024-07-16 21:17:32 +00:00
return ;
}
if ( TryGetApplicationsFromFile ( applicationPath , out List < ApplicationData > applications ) )
2023-11-12 02:35:30 +00:00
{
2024-07-16 21:17:32 +00:00
foreach ( var application in applications )
{
OnApplicationAdded ( new ApplicationAddedEventArgs
{
AppData = application ,
} ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
_applications . Edit ( it = >
{
foreach ( var application in applications )
{
it . AddOrUpdate ( application ) ;
}
} ) ;
2024-07-16 21:17:32 +00:00
if ( applications . Count > 1 )
{
numApplicationsFound + = applications . Count - 1 ;
}
numApplicationsLoaded + = applications . Count ;
}
else
{
numApplicationsFound - - ;
}
2022-05-15 11:30:15 +00:00
2023-06-29 00:39:22 +00:00
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs
2022-05-15 11:30:15 +00:00
{
NumAppsFound = numApplicationsFound ,
2023-06-29 00:39:22 +00:00
NumAppsLoaded = numApplicationsLoaded ,
2022-05-15 11:30:15 +00:00
} ) ;
}
2023-06-29 00:39:22 +00:00
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs
2022-05-15 11:30:15 +00:00
{
NumAppsFound = numApplicationsFound ,
2023-06-29 00:39:22 +00:00
NumAppsLoaded = numApplicationsLoaded ,
2022-05-15 11:30:15 +00:00
} ) ;
}
finally
{
_cancellationToken . Dispose ( ) ;
_cancellationToken = null ;
}
}
2024-08-17 03:26:25 +00:00
public void LoadDownloadableContents ( )
{
_downloadableContents . Edit ( it = >
{
it . Clear ( ) ;
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
foreach ( ApplicationData application in Applications . Items )
{
2024-08-17 19:18:08 +00:00
var savedDlc = DownloadableContentsHelper . LoadDownloadableContentsJson ( _virtualFileSystem , application . IdBase ) ;
it . AddOrUpdate ( savedDlc ) ;
if ( TryGetDownloadableContentFromFile ( application . Path , out var bundledDlc ) )
{
var savedDlcLookup = savedDlc . Select ( dlc = > dlc . Item1 ) . ToHashSet ( ) ;
bool addedNewDlc = false ;
foreach ( var dlc in bundledDlc )
{
if ( ! savedDlcLookup . Contains ( dlc ) )
{
addedNewDlc = true ;
it . AddOrUpdate ( ( dlc , true ) ) ;
}
}
if ( addedNewDlc )
{
var gameDlcs = it . Items . Where ( dlc = > dlc . Dlc . TitleIdBase = = application . IdBase ) . ToList ( ) ;
DownloadableContentsHelper . SaveDownloadableContentsJson ( _virtualFileSystem , application . IdBase , gameDlcs ) ;
}
}
2024-08-17 03:26:25 +00:00
}
} ) ;
}
2024-08-17 19:18:08 +00:00
2024-08-17 03:26:25 +00:00
public void SaveDownloadableContentsForGame ( ApplicationData application , List < ( DownloadableContentModel , bool IsEnabled ) > dlcs )
{
_downloadableContents . Edit ( it = >
{
DownloadableContentsHelper . SaveDownloadableContentsJson ( _virtualFileSystem , application . IdBase , dlcs ) ;
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
it . Remove ( it . Items . Where ( item = > item . Dlc . TitleIdBase = = application . IdBase ) ) ;
it . AddOrUpdate ( dlcs ) ;
} ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
// public void LoadTitleUpdates()
// {
//
// }
public void AutoLoadDownloadableContents ( List < string > appDirs )
2024-08-15 01:54:32 +00:00
{
_cancellationToken = new CancellationTokenSource ( ) ;
2024-08-17 03:26:25 +00:00
_downloadableContents . Clear ( ) ;
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
// Builds the applications list with paths to found applications
List < string > applicationPaths = new ( ) ;
try
{
foreach ( string appDir in appDirs )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
if ( ! Directory . Exists ( appDir ) )
{
Logger . Warning ? . Print ( LogClass . Application ,
$"The specified game directory \" { appDir } \ " does not exist." ) ;
continue ;
}
try
{
EnumerationOptions options = new ( )
{
2024-08-17 03:28:30 +00:00
RecurseSubdirectories = true ,
IgnoreInaccessible = false ,
2024-08-15 01:54:32 +00:00
} ;
IEnumerable < string > files = Directory . EnumerateFiles ( appDir , "*" , options ) . Where (
file = >
{
return
( Path . GetExtension ( file ) . ToLower ( ) is ".nsp" & &
ConfigurationState . Instance . UI . ShownFileTypes . NSP . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".xci" & &
ConfigurationState . Instance . UI . ShownFileTypes . XCI . Value ) ;
} ) ;
foreach ( string app in files )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
var fileInfo = new FileInfo ( app ) ;
try
{
var fullPath = fileInfo . ResolveLinkTarget ( true ) ? . FullName ? ? fileInfo . FullName ;
applicationPaths . Add ( fullPath ) ;
}
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application ,
$"Failed to resolve the full path to file: \" { app } \ " Error: {exception}" ) ;
}
}
}
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 ;
}
2024-08-16 02:17:37 +00:00
if ( TryGetDownloadableContentFromFile ( applicationPath , out List < DownloadableContentModel > downloadableContents ) )
2024-08-15 01:54:32 +00:00
{
2024-08-16 02:17:37 +00:00
foreach ( var downloadableContent in downloadableContents )
2024-08-15 01:54:32 +00:00
{
OnDownloadableContentAdded ( new DownloadableContentAddedEventArgs
{
2024-08-16 02:17:37 +00:00
DownloadableContent = downloadableContent ,
2024-08-15 01:54:32 +00:00
} ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
_downloadableContents . Edit ( it = >
{
foreach ( var downloadableContent in downloadableContents )
{
it . AddOrUpdate ( ( downloadableContent , true ) ) ;
}
} ) ;
2024-08-15 01:54:32 +00:00
}
}
}
finally
{
_cancellationToken . Dispose ( ) ;
_cancellationToken = null ;
}
}
2024-08-17 03:26:25 +00:00
public void AutoLoadTitleUpdates ( List < string > appDirs )
2024-08-15 01:54:32 +00:00
{
_cancellationToken = new CancellationTokenSource ( ) ;
2024-08-17 03:26:25 +00:00
_titleUpdates . Clear ( ) ;
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
// Builds the applications list with paths to found applications
List < string > applicationPaths = new ( ) ;
try
{
foreach ( string appDir in appDirs )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
if ( ! Directory . Exists ( appDir ) )
{
Logger . Warning ? . Print ( LogClass . Application ,
$"The specified game directory \" { appDir } \ " does not exist." ) ;
continue ;
}
try
{
EnumerationOptions options = new ( )
{
2024-08-17 03:28:30 +00:00
RecurseSubdirectories = true ,
IgnoreInaccessible = false ,
2024-08-15 01:54:32 +00:00
} ;
IEnumerable < string > files = Directory . EnumerateFiles ( appDir , "*" , options )
. Where ( file = >
{
return
( Path . GetExtension ( file ) . ToLower ( ) is ".nsp" & &
ConfigurationState . Instance . UI . ShownFileTypes . NSP . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".xci" & &
ConfigurationState . Instance . UI . ShownFileTypes . XCI . Value ) ;
} ) ;
foreach ( string app in files )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
var fileInfo = new FileInfo ( app ) ;
try
{
var fullPath = fileInfo . ResolveLinkTarget ( true ) ? . FullName ? ?
fileInfo . FullName ;
applicationPaths . Add ( fullPath ) ;
}
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application ,
$"Failed to resolve the full path to file: \" { app } \ " Error: {exception}" ) ;
}
}
}
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 ;
}
2024-08-16 02:17:37 +00:00
if ( TryGetTitleUpdatesFromFile ( applicationPath , out List < TitleUpdateModel > titleUpdates ) )
2024-08-15 01:54:32 +00:00
{
2024-08-16 02:17:37 +00:00
foreach ( var titleUpdate in titleUpdates )
2024-08-15 01:54:32 +00:00
{
OnTitleUpdateAdded ( new TitleUpdateAddedEventArgs ( )
{
2024-08-16 02:17:37 +00:00
TitleUpdate = titleUpdate ,
2024-08-15 01:54:32 +00:00
} ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-17 03:26:25 +00:00
_titleUpdates . Edit ( it = >
{
foreach ( var titleUpdate in titleUpdates )
{
it . AddOrUpdate ( ( titleUpdate , false ) ) ;
}
} ) ;
2024-08-15 01:54:32 +00:00
}
}
}
finally
{
_cancellationToken . Dispose ( ) ;
_cancellationToken = null ;
}
}
2022-05-15 11:30:15 +00:00
protected void OnApplicationAdded ( ApplicationAddedEventArgs e )
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
protected void OnApplicationCountUpdated ( ApplicationCountUpdatedEventArgs e )
{
ApplicationCountUpdated ? . Invoke ( null , e ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
protected void OnTitleUpdateAdded ( TitleUpdateAddedEventArgs e )
{
TitleUpdateAdded ? . Invoke ( null , e ) ;
}
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
protected void OnDownloadableContentAdded ( DownloadableContentAddedEventArgs e )
{
DownloadableContentAdded ? . Invoke ( null , e ) ;
}
2022-05-15 11:30:15 +00:00
2023-06-29 00:39:22 +00:00
public static ApplicationMetadata LoadAndSaveMetaData ( string titleId , Action < ApplicationMetadata > modifyFunction = null )
2022-05-15 11:30:15 +00:00
{
string metadataFolder = Path . Combine ( AppDataManager . GamesDirPath , titleId , "gui" ) ;
2023-06-29 00:39:22 +00:00
string metadataFile = Path . Combine ( metadataFolder , "metadata.json" ) ;
2022-05-15 11:30:15 +00:00
ApplicationMetadata appMetadata ;
if ( ! File . Exists ( metadataFile ) )
{
Directory . CreateDirectory ( metadataFolder ) ;
appMetadata = new ApplicationMetadata ( ) ;
2023-06-29 00:39:22 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
try
{
2023-06-29 00:39:22 +00:00
appMetadata = JsonHelper . DeserializeFromFile ( metadataFile , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
catch ( JsonException )
{
Logger . Warning ? . Print ( LogClass . Application , $"Failed to parse metadata json for {titleId}. Loading defaults." ) ;
appMetadata = new ApplicationMetadata ( ) ;
}
if ( modifyFunction ! = null )
{
modifyFunction ( appMetadata ) ;
2023-06-29 00:39:22 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
return appMetadata ;
}
2024-07-16 21:17:32 +00:00
public byte [ ] GetApplicationIcon ( string applicationPath , Language desiredTitleLanguage , ulong applicationId )
2022-05-15 11:30:15 +00:00
{
byte [ ] applicationIcon = null ;
2024-07-16 21:17:32 +00:00
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 ,
} ;
}
2022-05-15 11:30:15 +00:00
try
{
// Look for icon only if applicationPath is not a directory
if ( ! Directory . Exists ( applicationPath ) )
{
string extension = Path . GetExtension ( applicationPath ) . ToLower ( ) ;
2023-01-15 23:11:16 +00:00
using FileStream file = new ( applicationPath , FileMode . Open , FileAccess . Read ) ;
if ( extension = = ".nsp" | | extension = = ".pfs0" | | extension = = ".xci" )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
try
2022-05-15 11:30:15 +00:00
{
2023-10-22 23:30:46 +00:00
IFileSystem pfs ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
bool isExeFs = false ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( extension = = ".xci" )
{
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
pfs = xci . OpenPartition ( XciPartitionType . Secure ) ;
}
else
{
2023-10-22 23:30:46 +00:00
var pfsTemp = new PartitionFileSystem ( ) ;
pfsTemp . Initialize ( file . AsStorage ( ) ) . ThrowIfFailure ( ) ;
pfs = pfsTemp ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*" ) )
{
if ( Path . GetFileNameWithoutExtension ( fileEntry . FullPath ) = = "main" )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
isExeFs = true ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( isExeFs )
{
applicationIcon = _nspIcon ;
}
else
{
// Store the ControlFS in variable called controlFs
2024-07-16 21:17:32 +00:00
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 ) ;
}
}
2023-01-15 23:11:16 +00:00
// Read the icon from the ControlFS and store it as a byte array
try
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
using var icon = new UniqueRef < IFile > ( ) ;
2023-10-20 18:51:15 +00:00
controlFs . OpenFile ( ref icon . Ref , $"/icon_{desiredTitleLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2023-01-15 23:11:16 +00:00
using MemoryStream stream = new ( ) ;
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
catch ( HorizonResultException )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
foreach ( DirectoryEntryEx entry in controlFs . EnumerateEntries ( "/" , "*" ) )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
if ( entry . Name = = "control.nacp" )
{
continue ;
}
2022-05-15 11:30:15 +00:00
using var icon = new UniqueRef < IFile > ( ) ;
2023-03-02 02:42:27 +00:00
controlFs . OpenFile ( ref icon . Ref , entry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
using MemoryStream stream = new ( ) ;
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
2022-05-15 11:30:15 +00:00
2024-07-16 21:17:32 +00:00
break ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
applicationIcon ? ? = extension = = ".xci" ? _xciIcon : _nspIcon ;
2022-05-15 11:30:15 +00:00
}
}
}
2023-01-15 23:11:16 +00:00
catch ( MissingKeyException )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
applicationIcon = extension = = ".xci" ? _xciIcon : _nspIcon ;
}
catch ( InvalidDataException )
{
applicationIcon = extension = = ".xci" ? _xciIcon : _nspIcon ;
}
catch ( Exception exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}" ) ;
}
}
else if ( extension = = ".nro" )
{
BinaryReader reader = new ( file ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
byte [ ] Read ( long position , int size )
{
file . Seek ( position , SeekOrigin . Begin ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
return reader . ReadBytes ( size ) ;
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
try
{
file . Seek ( 24 , SeekOrigin . Begin ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
int assetOffset = reader . ReadInt32 ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( Encoding . ASCII . GetString ( Read ( assetOffset , 4 ) ) = = "ASET" )
{
byte [ ] iconSectionInfo = Read ( assetOffset + 8 , 0x10 ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
long iconOffset = BitConverter . ToInt64 ( iconSectionInfo , 0 ) ;
long iconSize = BitConverter . ToInt64 ( iconSectionInfo , 8 ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// Reads and stores game icon as byte array
2023-06-01 16:24:00 +00:00
if ( iconSize > 0 )
{
applicationIcon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
}
else
{
applicationIcon = _nroIcon ;
}
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
else
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
applicationIcon = _nroIcon ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
catch
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
else if ( extension = = ".nca" )
{
applicationIcon = _ncaIcon ;
}
// If its an NSO we just set defaults
else if ( extension = = ".nso" )
{
applicationIcon = _nsoIcon ;
}
2022-05-15 11:30:15 +00:00
}
}
2023-06-29 00:39:22 +00:00
catch ( Exception )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}" ) ;
2022-05-15 11:30:15 +00:00
}
return applicationIcon ? ? _ncaIcon ;
}
2024-07-16 21:17:32 +00:00
private void GetApplicationInformation ( ref ApplicationControlProperty controlData , ref ApplicationData data )
2022-05-15 11:30:15 +00:00
{
2024-08-03 21:31:34 +00:00
_ = Enum . TryParse ( DesiredLanguage . ToString ( ) , out TitleLanguage desiredTitleLanguage ) ;
2022-05-15 11:30:15 +00:00
if ( controlData . Title . ItemsRo . Length > ( int ) desiredTitleLanguage )
{
2024-07-16 21:17:32 +00:00
data . Name = controlData . Title [ ( int ) desiredTitleLanguage ] . NameString . ToString ( ) ;
data . Developer = controlData . Title [ ( int ) desiredTitleLanguage ] . PublisherString . ToString ( ) ;
2022-05-15 11:30:15 +00:00
}
else
{
2024-07-16 21:17:32 +00:00
data . Name = null ;
data . Developer = null ;
2022-05-15 11:30:15 +00:00
}
2024-07-16 21:17:32 +00:00
if ( string . IsNullOrWhiteSpace ( data . Name ) )
2022-05-15 11:30:15 +00:00
{
foreach ( ref readonly var controlTitle in controlData . Title . ItemsRo )
{
if ( ! controlTitle . NameString . IsEmpty ( ) )
{
2024-07-16 21:17:32 +00:00
data . Name = controlTitle . NameString . ToString ( ) ;
2022-05-15 11:30:15 +00:00
break ;
}
}
}
2024-07-16 21:17:32 +00:00
if ( string . IsNullOrWhiteSpace ( data . Developer ) )
2022-05-15 11:30:15 +00:00
{
foreach ( ref readonly var controlTitle in controlData . Title . ItemsRo )
{
if ( ! controlTitle . PublisherString . IsEmpty ( ) )
{
2024-07-16 21:17:32 +00:00
data . Developer = controlTitle . PublisherString . ToString ( ) ;
2022-05-15 11:30:15 +00:00
break ;
}
}
}
2024-07-16 21:17:32 +00:00
if ( data . Id = = 0 )
2022-05-15 11:30:15 +00:00
{
2024-07-16 21:17:32 +00:00
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 ) ;
}
2022-05-15 11:30:15 +00:00
}
2024-07-16 21:17:32 +00:00
data . Version = controlData . DisplayVersionString . ToString ( ) ;
2022-05-15 11:30:15 +00:00
}
2024-07-16 21:17:32 +00:00
private bool IsUpdateApplied ( Nca mainNca , out IFileSystem updatedControlFs )
2022-05-15 11:30:15 +00:00
{
updatedControlFs = null ;
2023-03-31 19:16:46 +00:00
2024-07-16 21:17:32 +00:00
string updatePath = null ;
2022-05-15 11:30:15 +00:00
try
{
2024-07-16 21:17:32 +00:00
( Nca patchNca , Nca controlNca ) = mainNca . GetUpdateData ( _virtualFileSystem , _checkLevel , 0 , out updatePath ) ;
2022-05-15 11:30:15 +00:00
if ( patchNca ! = null & & controlNca ! = null )
{
2024-07-16 21:17:32 +00:00
updatedControlFs = controlNca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
2022-05-15 11:30:15 +00:00
return true ;
}
}
catch ( InvalidDataException )
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}" ) ;
2022-05-15 11:30:15 +00:00
}
catch ( MissingKeyException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}" ) ;
}
return false ;
}
2024-08-17 03:28:30 +00:00
2024-08-15 01:54:32 +00:00
private Nca TryOpenNca ( IStorage ncaStorage )
{
try
{
return new Nca ( _virtualFileSystem . KeySet , ncaStorage ) ;
}
2024-08-16 03:41:12 +00:00
catch ( Exception ) { }
2024-08-15 01:54:32 +00:00
return null ;
}
2022-05-15 11:30:15 +00:00
}
2023-04-15 16:11:24 +00:00
}