2022-05-15 11:30:15 +00:00
using LibHac ;
using LibHac.Common ;
using LibHac.Common.Keys ;
using LibHac.Fs ;
using LibHac.Fs.Fsa ;
using LibHac.FsSystem ;
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 ;
2023-04-16 01:03:35 +00:00
using Ryujinx.Ui.Common.Configuration ;
2022-05-15 11:30:15 +00:00
using Ryujinx.Ui.Common.Configuration.System ;
using System ;
using System.Collections.Generic ;
2023-04-16 01:03:35 +00:00
using System.Linq ;
2023-03-31 19:16:46 +00:00
using System.Globalization ;
2022-05-15 11:30:15 +00:00
using System.IO ;
using System.Reflection ;
using System.Text ;
using System.Text.Json ;
using System.Threading ;
using Path = System . IO . Path ;
namespace Ryujinx.Ui.App.Common
{
public class ApplicationLibrary
{
public event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
public event EventHandler < ApplicationCountUpdatedEventArgs > ApplicationCountUpdated ;
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 ;
private Language _desiredTitleLanguage ;
private CancellationTokenSource _cancellationToken ;
2022-05-15 11:30:15 +00:00
2023-04-03 10:14:19 +00:00
private static readonly ApplicationJsonSerializerContext SerializerContext = new ( JsonHelper . GetDefaultSerializerOptions ( ) ) ;
private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new ( JsonHelper . GetDefaultSerializerOptions ( ) ) ;
2022-05-15 11:30:15 +00:00
public ApplicationLibrary ( VirtualFileSystem virtualFileSystem )
{
_virtualFileSystem = virtualFileSystem ;
_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" ) ;
}
2023-01-15 23:11:16 +00:00
private static byte [ ] GetResourceBytes ( string resourceName )
2022-05-15 11:30:15 +00:00
{
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
resourceStream . Read ( resourceByteArray ) ;
return resourceByteArray ;
}
public void CancelLoading ( )
{
_cancellationToken ? . Cancel ( ) ;
}
2023-01-15 23:11:16 +00:00
public static void ReadControlData ( IFileSystem controlFs , Span < byte > outProperty )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
using UniqueRef < IFile > controlFile = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-03-02 02:42:27 +00:00
controlFs . OpenFile ( ref controlFile . Ref , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
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
2023-01-15 23:11:16 +00:00
List < string > applications = new ( ) ;
2022-05-15 11:30:15 +00:00
try
{
foreach ( string appDir in appDirs )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
if ( ! Directory . Exists ( appDir ) )
{
Logger . Warning ? . Print ( LogClass . Application , $"The \" game_dirs \ " section in \"Config.json\" contains an invalid directory: \"{appDir}\"" ) ;
continue ;
}
2022-12-29 15:52:30 +00:00
try
2022-05-15 11:30:15 +00:00
{
2023-04-16 01:03:35 +00:00
IEnumerable < string > files = Directory . EnumerateFiles ( appDir , "*" , SearchOption . AllDirectories ) . Where ( file = >
{
return
( Path . GetExtension ( file ) . ToLower ( ) is ".nsp" & & ConfigurationState . Instance . Ui . ShownFileTypes . NSP . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".pfs0" & & ConfigurationState . Instance . Ui . ShownFileTypes . PFS0 . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".xci" & & ConfigurationState . Instance . Ui . ShownFileTypes . XCI . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".nca" & & ConfigurationState . Instance . Ui . ShownFileTypes . NCA . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".nro" & & ConfigurationState . Instance . Ui . ShownFileTypes . NRO . Value ) | |
( Path . GetExtension ( file ) . ToLower ( ) is ".nso" & & ConfigurationState . Instance . Ui . ShownFileTypes . NSO . Value ) ;
} ) ;
foreach ( string app in files )
2022-05-15 11:30:15 +00:00
{
2022-12-29 15:52:30 +00:00
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
2023-03-31 19:16:46 +00:00
2023-04-15 16:11:24 +00:00
var fileInfo = new FileInfo ( app ) ;
string extension = fileInfo . Extension . ToLower ( ) ;
2023-03-31 19:16:46 +00:00
2023-04-15 16:11:24 +00:00
if ( ! fileInfo . Attributes . HasFlag ( FileAttributes . Hidden ) & & extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso" )
2022-12-29 15:52:30 +00:00
{
2023-04-15 16:11:24 +00:00
var fullPath = fileInfo . ResolveLinkTarget ( true ) ? . FullName ? ? fileInfo . FullName ;
applications . Add ( fullPath ) ;
2022-12-29 15:52:30 +00:00
numApplicationsFound + + ;
}
2022-05-15 11:30:15 +00:00
}
}
2022-12-29 15:52:30 +00:00
catch ( UnauthorizedAccessException )
{
Logger . Warning ? . Print ( LogClass . Application , $"Failed to get access to directory: \" { appDir } \ "" ) ;
}
2022-05-15 11:30:15 +00:00
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach ( string applicationPath in applications )
{
if ( _cancellationToken . Token . IsCancellationRequested )
{
return ;
}
double fileSize = new FileInfo ( applicationPath ) . Length * 0.000000000931 ;
string titleName = "Unknown" ;
string titleId = "0000000000000000" ;
string developer = "Unknown" ;
string version = "0" ;
byte [ ] applicationIcon = null ;
2023-01-15 23:11:16 +00:00
BlitStruct < ApplicationControlProperty > controlHolder = new ( 1 ) ;
2022-05-15 11:30:15 +00:00
try
{
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-01-15 23:11:16 +00:00
PartitionFileSystem 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
{
pfs = new PartitionFileSystem ( file . AsStorage ( ) ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +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
2023-01-15 23:11:16 +00:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*" ) )
{
if ( Path . GetExtension ( fileEntry . FullPath ) . ToLower ( ) = = ".nca" )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
using UniqueRef < IFile > ncaFile = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-03-02 02:42:27 +00:00
pfs . OpenFile ( ref ncaFile . Ref , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
Nca nca = new ( _virtualFileSystem . KeySet , ncaFile . Get . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +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 ( ) ) )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
hasMainNca = true ;
break ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
else 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
if ( ! hasMainNca & & ! isExeFs )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
numApplicationsFound - - ;
continue ;
}
}
if ( isExeFs )
{
applicationIcon = _nspIcon ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
using UniqueRef < IFile > npdmFile = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-03-02 02:42:27 +00:00
Result result = pfs . OpenFile ( ref npdmFile . Ref , "/main.npdm" . ToU8Span ( ) , OpenMode . Read ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( ResultFs . PathNotFound . Includes ( result ) )
{
Npdm npdm = new ( npdmFile . Get . AsStream ( ) ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
titleName = npdm . TitleName ;
titleId = npdm . Aci0 . TitleId . ToString ( "x16" ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
}
else
{
GetControlFsAndTitleId ( pfs , out IFileSystem controlFs , out titleId ) ;
// Check if there is an update available.
if ( IsUpdateApplied ( titleId , out IFileSystem updatedControlFs ) )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs ;
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
ReadControlData ( controlFs , controlHolder . ByteSpan ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
GetGameInformation ( ref controlHolder . Value , out titleName , out _ , out developer , out version ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// Read the icon from the ControlFS and store it as a byte array
try
{
using UniqueRef < IFile > icon = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-03-02 02:42:27 +00:00
controlFs . OpenFile ( ref icon . Ref , $"/icon_{_desiredTitleLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
using MemoryStream stream = new ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
catch ( HorizonResultException )
{
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" )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
continue ;
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
using var icon = new UniqueRef < IFile > ( ) ;
2022-05-15 11:30:15 +00:00
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
2023-01-15 23:11:16 +00:00
using MemoryStream stream = new ( ) ;
2023-03-31 19:16:46 +00:00
2023-01-15 23:11:16 +00:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( applicationIcon ! = null )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +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 exception )
{
applicationIcon = extension = = ".xci" ? _xciIcon : _nspIcon ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
}
catch ( InvalidDataException )
{
applicationIcon = extension = = ".xci" ? _xciIcon : _nspIcon ;
2022-05-15 11:30:15 +00:00
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: {applicationPath}" ) ;
}
catch ( Exception exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}" ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
numApplicationsFound - - ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
continue ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
}
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
ulong nacpOffset = reader . ReadUInt64 ( ) ;
ulong nacpSize = reader . ReadUInt64 ( ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// Reads and stores game icon as byte array
applicationIcon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// Read the NACP data
Read ( assetOffset + ( int ) nacpOffset , ( int ) nacpSize ) . AsSpan ( ) . CopyTo ( controlHolder . ByteSpan ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
GetGameInformation ( ref controlHolder . Value , out titleName , out titleId , out developer , out version ) ;
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 ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
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
numApplicationsFound - - ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
continue ;
}
}
else if ( extension = = ".nca" )
{
try
{
Nca nca = new ( _virtualFileSystem . KeySet , new FileStream ( applicationPath , FileMode . Open , FileAccess . Read ) . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( nca . Header . ContentType ! = NcaContentType . Program | | ( nca . SectionExists ( NcaSectionType . Data ) & & nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) ) )
{
2022-05-15 11:30:15 +00:00
numApplicationsFound - - ;
continue ;
}
}
2023-01-15 23:11:16 +00:00
catch ( InvalidDataException )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}" ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
catch
{
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
numApplicationsFound - - ;
continue ;
}
applicationIcon = _ncaIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
}
// If its an NSO we just set defaults
else if ( extension = = ".nso" )
{
applicationIcon = _nsoIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2022-05-15 11:30:15 +00:00
}
}
catch ( IOException exception )
{
Logger . Warning ? . Print ( LogClass . Application , exception . Message ) ;
numApplicationsFound - - ;
continue ;
}
2022-12-02 13:16:43 +00:00
ApplicationMetadata appMetadata = LoadAndSaveMetaData ( titleId , appMetadata = >
{
appMetadata . Title = titleName ;
} ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( appMetadata . LastPlayed ! = "Never" )
2023-03-31 19:16:46 +00:00
{
2023-01-15 23:11:16 +00:00
if ( ! DateTime . TryParse ( appMetadata . LastPlayed , out _ ) )
{
Logger . Warning ? . Print ( LogClass . Application , $"Last played datetime \" { appMetadata . LastPlayed } \ " is invalid for current system culture, skipping (did current culture change?)" ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
appMetadata . LastPlayed = "Never" ;
}
else
{
appMetadata . LastPlayed = appMetadata . LastPlayed [ . . ^ 3 ] ;
}
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
ApplicationData data = new ( )
2022-05-15 11:30:15 +00:00
{
Favorite = appMetadata . Favorite ,
Icon = applicationIcon ,
TitleName = titleName ,
TitleId = titleId ,
Developer = developer ,
Version = version ,
2023-01-15 23:11:16 +00:00
TimePlayed = ConvertSecondsToFormattedString ( appMetadata . TimePlayed ) ,
2022-12-06 01:40:06 +00:00
TimePlayedNum = appMetadata . TimePlayed ,
2022-05-15 11:30:15 +00:00
LastPlayed = appMetadata . LastPlayed ,
FileExtension = Path . GetExtension ( applicationPath ) . ToUpper ( ) . Remove ( 0 , 1 ) ,
2022-12-05 22:04:18 +00:00
FileSize = ( fileSize < 1 ) ? ( fileSize * 1024 ) . ToString ( "0.##" ) + " MiB" : fileSize . ToString ( "0.##" ) + " GiB" ,
2022-12-06 01:40:06 +00:00
FileSizeBytes = fileSize ,
2022-05-15 11:30:15 +00:00
Path = applicationPath ,
ControlHolder = controlHolder
} ;
numApplicationsLoaded + + ;
OnApplicationAdded ( new ApplicationAddedEventArgs ( )
{
AppData = data
} ) ;
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs ( )
{
NumAppsFound = numApplicationsFound ,
NumAppsLoaded = numApplicationsLoaded
} ) ;
}
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs ( )
{
NumAppsFound = numApplicationsFound ,
NumAppsLoaded = numApplicationsLoaded
} ) ;
}
finally
{
_cancellationToken . Dispose ( ) ;
_cancellationToken = null ;
}
}
protected void OnApplicationAdded ( ApplicationAddedEventArgs e )
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
protected void OnApplicationCountUpdated ( ApplicationCountUpdatedEventArgs e )
{
ApplicationCountUpdated ? . Invoke ( null , e ) ;
}
private void GetControlFsAndTitleId ( PartitionFileSystem pfs , out IFileSystem controlFs , out string titleId )
{
2023-03-31 19:16:46 +00:00
( _ , _ , Nca controlNca ) = GetGameData ( _virtualFileSystem , pfs , 0 ) ;
2022-05-15 11:30:15 +00:00
// Return the ControlFS
controlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
titleId = controlNca ? . Header . TitleId . ToString ( "x16" ) ;
}
public ApplicationMetadata LoadAndSaveMetaData ( string titleId , Action < ApplicationMetadata > modifyFunction = null )
{
string metadataFolder = Path . Combine ( AppDataManager . GamesDirPath , titleId , "gui" ) ;
string metadataFile = Path . Combine ( metadataFolder , "metadata.json" ) ;
ApplicationMetadata appMetadata ;
if ( ! File . Exists ( metadataFile ) )
{
Directory . CreateDirectory ( metadataFolder ) ;
appMetadata = new ApplicationMetadata ( ) ;
2023-04-03 10:14:19 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , SerializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
try
{
2023-04-03 10:14:19 +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-04-03 10:14:19 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , SerializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
return appMetadata ;
}
public byte [ ] GetApplicationIcon ( string applicationPath )
{
byte [ ] applicationIcon = null ;
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-01-15 23:11:16 +00:00
PartitionFileSystem 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
{
pfs = new PartitionFileSystem ( file . AsStorage ( ) ) ;
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
GetControlFsAndTitleId ( pfs , out IFileSystem controlFs , out _ ) ;
// 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-03-02 02:42:27 +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
2023-01-15 23:11:16 +00:00
using ( MemoryStream stream = new ( ) )
2022-05-15 11:30:15 +00:00
{
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
2023-01-15 23:11:16 +00:00
if ( applicationIcon ! = null )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +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
applicationIcon = 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
{
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
}
}
catch ( Exception )
{
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 ;
}
2023-01-15 23:11:16 +00:00
private static string ConvertSecondsToFormattedString ( double seconds )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
System . TimeSpan time = System . TimeSpan . FromSeconds ( seconds ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
string timeString ;
if ( time . Days ! = 0 )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m" ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
else if ( time . Hours ! = 0 )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
timeString = $"{time.Hours:D2}h {time.Minutes:D2}m" ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
else if ( time . Minutes ! = 0 )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
timeString = $"{time.Minutes:D2}m" ;
2022-05-15 11:30:15 +00:00
}
else
{
2023-01-15 23:11:16 +00:00
timeString = "Never" ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
return timeString ;
2022-05-15 11:30:15 +00:00
}
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 )
{
titleName = controlData . Title [ ( int ) desiredTitleLanguage ] . NameString . ToString ( ) ;
publisher = controlData . Title [ ( int ) desiredTitleLanguage ] . PublisherString . ToString ( ) ;
}
else
{
titleName = null ;
publisher = null ;
}
if ( string . IsNullOrWhiteSpace ( titleName ) )
{
foreach ( ref readonly var controlTitle in controlData . Title . ItemsRo )
{
if ( ! controlTitle . NameString . IsEmpty ( ) )
{
titleName = controlTitle . NameString . ToString ( ) ;
break ;
}
}
}
if ( string . IsNullOrWhiteSpace ( publisher ) )
{
foreach ( ref readonly var controlTitle in controlData . Title . ItemsRo )
{
if ( ! controlTitle . PublisherString . IsEmpty ( ) )
{
publisher = controlTitle . PublisherString . ToString ( ) ;
break ;
}
}
}
if ( controlData . PresenceGroupId ! = 0 )
{
titleId = controlData . PresenceGroupId . ToString ( "x16" ) ;
}
else if ( controlData . SaveDataOwnerId ! = 0 )
{
titleId = controlData . SaveDataOwnerId . ToString ( ) ;
}
else if ( controlData . AddOnContentBaseId ! = 0 )
{
titleId = ( controlData . AddOnContentBaseId - 0x1000 ) . ToString ( "x16" ) ;
}
else
{
titleId = "0000000000000000" ;
}
version = controlData . DisplayVersionString . ToString ( ) ;
}
private bool IsUpdateApplied ( string titleId , out IFileSystem updatedControlFs )
{
updatedControlFs = null ;
2023-03-31 19:16:46 +00:00
2022-05-15 11:30:15 +00:00
string updatePath = "(unknown)" ;
try
{
2023-03-31 19:16:46 +00:00
( Nca patchNca , Nca controlNca ) = GetGameUpdateData ( _virtualFileSystem , titleId , 0 , out updatePath ) ;
2022-05-15 11:30:15 +00:00
if ( patchNca ! = null & & controlNca ! = null )
{
updatedControlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
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 ;
}
2023-03-31 19:16:46 +00:00
public static ( Nca main , Nca patch , Nca control ) GetGameData ( VirtualFileSystem fileSystem , PartitionFileSystem 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 Nca ( 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 Nca ( 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 & = ~ 0xF UL ;
// Load update information if exists.
string titleUpdateMetadataPath = Path . Combine ( AppDataManager . GamesDirPath , titleIdBase . ToString ( "x16" ) , "updates.json" ) ;
if ( File . Exists ( titleUpdateMetadataPath ) )
{
2023-04-03 10:14:19 +00:00
updatePath = JsonHelper . DeserializeFromFile ( titleUpdateMetadataPath , TitleSerializerContext . TitleUpdateMetadata ) . Selected ;
2023-03-31 19:16:46 +00:00
if ( File . Exists ( updatePath ) )
{
FileStream file = new FileStream ( updatePath , FileMode . Open , FileAccess . Read ) ;
PartitionFileSystem nsp = new PartitionFileSystem ( file . AsStorage ( ) ) ;
return GetGameUpdateDataFromPartition ( fileSystem , nsp , titleIdBase . ToString ( "x16" ) , programIndex ) ;
}
}
}
return ( null , null ) ;
}
2022-05-15 11:30:15 +00:00
}
2023-04-15 16:11:24 +00:00
}