2019-11-29 05:32:51 +01:00
using LibHac ;
2020-03-25 09:14:35 +01:00
using LibHac.Common ;
2019-09-02 18:03:57 +02:00
using LibHac.Fs ;
2020-01-05 12:49:44 +01:00
using LibHac.Fs.Shim ;
2019-10-17 08:17:44 +02:00
using LibHac.FsSystem ;
using LibHac.FsSystem.NcaUtils ;
2020-01-05 12:49:44 +01:00
using LibHac.Ncm ;
2020-03-25 18:09:38 +01:00
using LibHac.Ns ;
2019-10-17 08:17:44 +02:00
using LibHac.Spl ;
2019-09-02 18:03:57 +02:00
using Ryujinx.Common.Logging ;
2020-04-12 23:02:37 +02:00
using Ryujinx.Common.Configuration ;
2020-01-21 23:23:11 +01:00
using Ryujinx.Configuration.System ;
2019-11-29 05:32:51 +01:00
using Ryujinx.HLE.FileSystem ;
using Ryujinx.HLE.Loaders.Npdm ;
2019-09-02 18:03:57 +02:00
using System ;
using System.Collections.Generic ;
2020-01-05 12:49:44 +01:00
using System.Globalization ;
2019-09-02 18:03:57 +02:00
using System.IO ;
using System.Reflection ;
using System.Text ;
2020-04-30 14:07:41 +02:00
using System.Text.Json ;
2019-10-17 08:17:44 +02:00
2020-01-05 12:49:44 +01:00
using RightsId = LibHac . Fs . RightsId ;
2020-04-30 14:07:41 +02:00
using JsonHelper = Ryujinx . Common . Utilities . JsonHelper ;
2019-09-02 18:03:57 +02:00
2019-11-29 05:32:51 +01:00
namespace Ryujinx.Ui
2019-09-02 18:03:57 +02:00
{
public class ApplicationLibrary
{
2020-01-31 19:21:46 +01:00
public static event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
public static event EventHandler < ApplicationCountUpdatedEventArgs > ApplicationCountUpdated ;
2019-09-02 18:03:57 +02:00
2019-11-29 05:32:51 +01:00
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" ) ;
2019-09-02 18:03:57 +02:00
2020-01-24 17:01:21 +01:00
private static VirtualFileSystem _virtualFileSystem ;
private static Language _desiredTitleLanguage ;
2020-01-31 19:21:46 +01:00
private static bool _loadingError ;
2019-09-02 18:03:57 +02:00
2020-03-25 17:17:54 +01:00
public static IEnumerable < string > GetFilesInDirectory ( string directory )
{
Stack < string > stack = new Stack < string > ( ) ;
stack . Push ( directory ) ;
while ( stack . Count > 0 )
{
string dir = stack . Pop ( ) ;
string [ ] content = { } ;
try
{
content = Directory . GetFiles ( dir , "*" ) ;
}
catch ( UnauthorizedAccessException )
{
Logger . PrintWarning ( LogClass . Application , $"Failed to get access to directory: \" { dir } \ "" ) ;
}
if ( content . Length > 0 )
{
foreach ( string file in content )
yield return file ;
}
try
{
content = Directory . GetDirectories ( dir ) ;
}
catch ( UnauthorizedAccessException )
{
Logger . PrintWarning ( LogClass . Application , $"Failed to get access to directory: \" { dir } \ "" ) ;
}
if ( content . Length > 0 )
{
foreach ( string subdir in content )
stack . Push ( subdir ) ;
}
}
}
2020-03-25 18:09:38 +01:00
public static void ReadControlData ( IFileSystem controlFs , Span < byte > outProperty )
{
controlFs . OpenFile ( out IFile controlFile , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-05-03 01:43:22 +02:00
controlFile . Read ( out _ , 0 , outProperty , ReadOption . None ) . ThrowIfFailure ( ) ;
2020-03-25 18:09:38 +01:00
}
2020-01-21 23:23:11 +01:00
public static void LoadApplications ( List < string > appDirs , VirtualFileSystem virtualFileSystem , Language desiredTitleLanguage )
2019-09-02 18:03:57 +02:00
{
2019-11-29 05:32:51 +01:00
int numApplicationsFound = 0 ;
int numApplicationsLoaded = 0 ;
2019-09-02 18:03:57 +02:00
2020-01-31 19:21:46 +01:00
_loadingError = false ;
2020-01-24 17:01:21 +01:00
_virtualFileSystem = virtualFileSystem ;
2019-11-29 05:32:51 +01:00
_desiredTitleLanguage = desiredTitleLanguage ;
2019-09-02 18:03:57 +02:00
// Builds the applications list with paths to found applications
List < string > applications = new List < string > ( ) ;
2019-11-29 05:32:51 +01:00
foreach ( string appDir in appDirs )
2019-09-02 18:03:57 +02:00
{
2020-03-25 17:17:54 +01:00
2020-01-31 19:21:46 +01:00
if ( ! Directory . Exists ( appDir ) )
2019-09-02 18:03:57 +02:00
{
Logger . PrintWarning ( LogClass . Application , $"The \" game_dirs \ " section in \"Config.json\" contains an invalid directory: \"{appDir}\"" ) ;
continue ;
}
2020-03-25 17:17:54 +01:00
foreach ( string app in GetFilesInDirectory ( appDir ) )
2019-09-02 18:03:57 +02:00
{
2020-01-31 19:21:46 +01:00
if ( ( Path . GetExtension ( app ) . ToLower ( ) = = ".nsp" ) | |
2020-03-25 17:17:54 +01:00
( Path . GetExtension ( app ) . ToLower ( ) = = ".pfs0" ) | |
2020-01-31 19:21:46 +01:00
( Path . GetExtension ( app ) . ToLower ( ) = = ".xci" ) | |
( Path . GetExtension ( app ) . ToLower ( ) = = ".nca" ) | |
( Path . GetExtension ( app ) . ToLower ( ) = = ".nro" ) | |
( Path . GetExtension ( app ) . ToLower ( ) = = ".nso" ) )
2019-09-02 18:03:57 +02:00
{
2019-11-29 05:32:51 +01:00
applications . Add ( app ) ;
numApplicationsFound + + ;
}
2019-09-02 18:03:57 +02:00
}
}
2019-11-29 05:32:51 +01:00
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
2019-09-02 18:03:57 +02:00
foreach ( string applicationPath in applications )
{
2019-11-29 05:32:51 +01:00
double fileSize = new FileInfo ( applicationPath ) . Length * 0.000000000931 ;
string titleName = "Unknown" ;
string titleId = "0000000000000000" ;
string developer = "Unknown" ;
string version = "0" ;
2020-01-05 12:49:44 +01:00
string saveDataPath = null ;
2019-09-02 18:03:57 +02:00
byte [ ] applicationIcon = null ;
2020-03-25 18:09:38 +01:00
BlitStruct < ApplicationControlProperty > controlHolder = new BlitStruct < ApplicationControlProperty > ( 1 ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
try
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
using ( FileStream file = new FileStream ( applicationPath , FileMode . Open , FileAccess . Read ) )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
if ( ( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".nsp" ) | |
( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".pfs0" ) | |
( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".xci" ) )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
try
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
PartitionFileSystem pfs ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
bool isExeFs = false ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
if ( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".xci" )
{
Xci xci = new Xci ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
pfs = xci . OpenPartition ( XciPartitionType . Secure ) ;
}
else
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
pfs = new PartitionFileSystem ( file . AsStorage ( ) ) ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01: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 ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*" ) )
{
if ( Path . GetExtension ( fileEntry . FullPath ) . ToLower ( ) = = ".nca" )
2020-01-31 19:21:46 +01:00
{
2020-03-25 09:14:35 +01:00
pfs . OpenFile ( out IFile ncaFile , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
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 ;
2020-01-31 19:21:46 +01:00
}
}
2020-02-15 21:20:19 +01:00
if ( ! hasMainNca & & ! isExeFs )
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
numApplicationsFound - - ;
continue ;
2020-01-31 19:21:46 +01:00
}
}
2020-02-15 21:20:19 +01:00
if ( isExeFs )
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
applicationIcon = _nspIcon ;
2019-10-17 08:17:44 +02:00
2020-03-25 09:14:35 +01:00
Result result = pfs . OpenFile ( out IFile npdmFile , "/main.npdm" . ToU8Span ( ) , OpenMode . Read ) ;
2019-09-02 18:03:57 +02:00
2020-03-03 15:07:06 +01:00
if ( ResultFs . PathNotFound . Includes ( result ) )
2020-02-15 21:20:19 +01:00
{
Npdm npdm = new Npdm ( npdmFile . AsStream ( ) ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
titleName = npdm . TitleName ;
titleId = npdm . Aci0 . TitleId . ToString ( "x16" ) ;
}
2019-11-29 05:32:51 +01:00
}
2020-02-15 21:20:19 +01:00
else
{
// Store the ControlFS in variable called controlFs
GetControlFsAndTitleId ( pfs , out IFileSystem controlFs , out titleId ) ;
2019-09-02 18:03:57 +02:00
2020-03-25 18:09:38 +01:00
ReadControlData ( controlFs , controlHolder . ByteSpan ) ;
2020-02-15 21:20:19 +01:00
// Creates NACP class from the NACP file
2020-03-25 09:14:35 +01:00
controlFs . OpenFile ( out IFile controlNacpFile , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
// Get the title name, title ID, developer name and version number from the NACP
2020-04-12 23:02:37 +02:00
version = IsUpdateApplied ( titleId , out string updateVersion ) ? updateVersion : controlHolder . Value . DisplayVersion . ToString ( ) ;
2019-09-02 18:03:57 +02:00
2020-04-03 12:01:26 +02:00
GetNameIdDeveloper ( ref controlHolder . Value , out titleName , out _ , out developer ) ;
2019-10-17 08:17:44 +02:00
2020-02-15 21:20:19 +01:00
// Read the icon from the ControlFS and store it as a byte array
try
2019-09-02 18:03:57 +02:00
{
2020-03-25 09:14:35 +01:00
controlFs . OpenFile ( out IFile icon , $"/icon_{_desiredTitleLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-11-29 05:32:51 +01:00
using ( MemoryStream stream = new MemoryStream ( ) )
{
icon . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
2020-02-15 21:20:19 +01:00
}
catch ( HorizonResultException )
{
foreach ( DirectoryEntryEx entry in controlFs . EnumerateEntries ( "/" , "*" ) )
2019-11-29 05:32:51 +01:00
{
2020-02-15 21:20:19 +01:00
if ( entry . Name = = "control.nacp" )
{
continue ;
}
2020-03-25 09:14:35 +01:00
controlFs . OpenFile ( out IFile icon , entry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-02-15 21:20:19 +01:00
using ( MemoryStream stream = new MemoryStream ( ) )
{
icon . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
if ( applicationIcon ! = null )
{
break ;
}
2019-11-29 05:32:51 +01:00
}
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
if ( applicationIcon = = null )
{
applicationIcon = Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".xci" ? _xciIcon : _nspIcon ;
}
2019-11-29 05:32:51 +01:00
}
2019-09-02 18:03:57 +02:00
}
}
2020-02-15 21:20:19 +01:00
catch ( MissingKeyException exception )
{
applicationIcon = Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".xci" ? _xciIcon : _nspIcon ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
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 ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
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 . PrintWarning ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
Logger . PrintDebug ( LogClass . Application , exception . ToString ( ) ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
numApplicationsFound - - ;
_loadingError = true ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
continue ;
}
2019-09-02 18:03:57 +02:00
}
2020-02-15 21:20:19 +01:00
else if ( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".nro" )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
BinaryReader reader = new BinaryReader ( file ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
byte [ ] Read ( long position , int size )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
file . Seek ( position , SeekOrigin . Begin ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
return reader . ReadBytes ( size ) ;
}
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
try
{
file . Seek ( 24 , SeekOrigin . Begin ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
int assetOffset = reader . ReadInt32 ( ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
if ( Encoding . ASCII . GetString ( Read ( assetOffset , 4 ) ) = = "ASET" )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
byte [ ] iconSectionInfo = Read ( assetOffset + 8 , 0x10 ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
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 ) ;
2020-04-03 12:01:26 +02:00
// Read the NACP data
Read ( assetOffset + ( int ) nacpOffset , ( int ) nacpSize ) . AsSpan ( ) . CopyTo ( controlHolder . ByteSpan ) ;
2019-09-02 18:03:57 +02:00
2020-04-03 12:01:26 +02:00
// Get the title name, title ID, developer name and version number from the NACP
version = controlHolder . Value . DisplayVersion . ToString ( ) ;
2020-02-15 21:20:19 +01:00
2020-04-03 12:01:26 +02:00
GetNameIdDeveloper ( ref controlHolder . Value , out titleName , out titleId , out developer ) ;
2020-02-15 21:20:19 +01:00
}
else
{
applicationIcon = _nroIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2019-09-02 18:03:57 +02:00
}
2020-01-31 19:21:46 +01:00
}
2020-02-15 21:20:19 +01:00
catch
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
Logger . PrintWarning ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
numApplicationsFound - - ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
continue ;
}
2020-01-31 19:21:46 +01:00
}
2020-02-15 21:20:19 +01:00
else if ( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".nca" )
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
try
{
Nca nca = new Nca ( _virtualFileSystem . KeySet , new FileStream ( applicationPath , FileMode . Open , FileAccess . Read ) . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
if ( nca . Header . ContentType ! = NcaContentType . Program | | nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
{
numApplicationsFound - - ;
continue ;
}
}
catch ( InvalidDataException )
2020-01-31 19:21:46 +01:00
{
2020-02-15 21:20:19 +01:00
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 . PrintWarning ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
2020-01-31 19:21:46 +01:00
numApplicationsFound - - ;
2020-02-15 21:20:19 +01:00
_loadingError = true ;
2019-09-02 18:03:57 +02:00
2020-01-31 19:21:46 +01:00
continue ;
2019-09-02 18:03:57 +02:00
}
2020-02-15 21:20:19 +01:00
applicationIcon = _ncaIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2019-09-02 18:03:57 +02:00
}
2020-02-15 21:20:19 +01:00
// If its an NSO we just set defaults
else if ( Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".nso" )
2019-09-02 18:03:57 +02:00
{
2020-02-15 21:20:19 +01:00
applicationIcon = _nsoIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2019-09-02 18:03:57 +02:00
}
2020-02-15 21:20:19 +01:00
}
}
catch ( IOException exception )
{
Logger . PrintWarning ( LogClass . Application , exception . Message ) ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
numApplicationsFound - - ;
_loadingError = true ;
2020-01-31 19:21:46 +01:00
2020-02-15 21:20:19 +01:00
continue ;
2019-09-02 18:03:57 +02:00
}
2020-01-12 04:01:04 +01:00
ApplicationMetadata appMetadata = LoadAndSaveMetaData ( titleId ) ;
2019-09-02 18:03:57 +02:00
2020-01-05 12:49:44 +01:00
if ( ulong . TryParse ( titleId , NumberStyles . HexNumber , CultureInfo . InvariantCulture , out ulong titleIdNum ) )
{
SaveDataFilter filter = new SaveDataFilter ( ) ;
filter . SetUserId ( new UserId ( 1 , 0 ) ) ;
2020-03-03 15:07:06 +01:00
filter . SetProgramId ( new TitleId ( titleIdNum ) ) ;
2020-01-05 12:49:44 +01:00
2020-01-21 23:23:11 +01:00
Result result = virtualFileSystem . FsClient . FindSaveDataWithFilter ( out SaveDataInfo saveDataInfo , SaveDataSpaceId . User , ref filter ) ;
2020-01-05 12:49:44 +01:00
if ( result . IsSuccess ( ) )
{
2020-04-12 23:02:37 +02:00
saveDataPath = Path . Combine ( virtualFileSystem . GetNandPath ( ) , "user" , "save" , saveDataInfo . SaveDataId . ToString ( "x16" ) ) ;
2020-01-05 12:49:44 +01:00
}
}
2020-04-12 23:02:37 +02:00
ApplicationData data = new ApplicationData
2019-09-02 18:03:57 +02:00
{
2020-01-12 04:01:04 +01:00
Favorite = appMetadata . Favorite ,
2019-11-29 05:32:51 +01:00
Icon = applicationIcon ,
TitleName = titleName ,
TitleId = titleId ,
Developer = developer ,
Version = version ,
2020-01-12 04:01:04 +01:00
TimePlayed = ConvertSecondsToReadableString ( appMetadata . TimePlayed ) ,
LastPlayed = appMetadata . LastPlayed ,
2020-05-03 04:00:53 +02:00
FileExtension = Path . GetExtension ( applicationPath ) . ToUpper ( ) . Remove ( 0 , 1 ) ,
2019-11-29 05:32:51 +01:00
FileSize = ( fileSize < 1 ) ? ( fileSize * 1024 ) . ToString ( "0.##" ) + "MB" : fileSize . ToString ( "0.##" ) + "GB" ,
Path = applicationPath ,
2020-03-25 18:09:38 +01:00
SaveDataPath = saveDataPath ,
ControlHolder = controlHolder
2019-09-02 18:03:57 +02:00
} ;
2019-11-29 05:32:51 +01:00
numApplicationsLoaded + + ;
OnApplicationAdded ( new ApplicationAddedEventArgs ( )
2020-01-31 19:21:46 +01:00
{
AppData = data
} ) ;
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs ( )
{
2019-11-29 05:32:51 +01:00
NumAppsFound = numApplicationsFound ,
NumAppsLoaded = numApplicationsLoaded
} ) ;
2019-09-02 18:03:57 +02:00
}
2020-01-31 19:21:46 +01:00
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs ( )
{
NumAppsFound = numApplicationsFound ,
NumAppsLoaded = numApplicationsLoaded
} ) ;
if ( _loadingError )
{
Gtk . Application . Invoke ( delegate
{
2020-02-15 21:20:19 +01:00
GtkDialog . CreateErrorDialog ( "One or more files encountered could not be loaded, check logs for more info." ) ;
2020-01-31 19:21:46 +01:00
} ) ;
}
2019-09-02 18:03:57 +02:00
}
2019-11-29 05:32:51 +01:00
protected static void OnApplicationAdded ( ApplicationAddedEventArgs e )
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
2020-01-31 19:21:46 +01:00
protected static void OnApplicationCountUpdated ( ApplicationCountUpdatedEventArgs e )
{
ApplicationCountUpdated ? . Invoke ( null , e ) ;
}
2019-09-02 18:03:57 +02:00
private static byte [ ] GetResourceBytes ( string resourceName )
{
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
resourceStream . Read ( resourceByteArray ) ;
return resourceByteArray ;
}
2020-02-11 23:43:24 +01:00
private static void GetControlFsAndTitleId ( PartitionFileSystem pfs , out IFileSystem controlFs , out string titleId )
2019-09-02 18:03:57 +02:00
{
Nca controlNca = null ;
2019-11-29 05:32:51 +01:00
// Add keys to key set if needed
2020-05-15 08:16:46 +02:00
_virtualFileSystem . ImportTickets ( pfs ) ;
2019-09-02 18:03:57 +02:00
// Find the Control NCA and store it in variable called controlNca
2019-11-29 05:32:51 +01:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*.nca" ) )
2019-09-02 18:03:57 +02:00
{
2020-03-25 09:14:35 +01:00
pfs . OpenFile ( out IFile ncaFile , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-10-17 08:17:44 +02:00
2020-01-24 17:01:21 +01:00
Nca nca = new Nca ( _virtualFileSystem . KeySet , ncaFile . AsStorage ( ) ) ;
2019-10-17 08:17:44 +02:00
if ( nca . Header . ContentType = = NcaContentType . Control )
2019-09-02 18:03:57 +02:00
{
controlNca = nca ;
}
}
// Return the ControlFS
2020-02-11 23:43:24 +01:00
controlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
titleId = controlNca ? . Header . TitleId . ToString ( "x16" ) ;
2019-09-02 18:03:57 +02:00
}
2020-01-12 04:01:04 +01:00
internal static ApplicationMetadata LoadAndSaveMetaData ( string titleId , Action < ApplicationMetadata > modifyFunction = null )
2019-09-02 18:03:57 +02:00
{
2020-01-24 17:01:21 +01:00
string metadataFolder = Path . Combine ( _virtualFileSystem . GetBasePath ( ) , "games" , titleId , "gui" ) ;
2020-01-31 19:21:46 +01:00
string metadataFile = Path . Combine ( metadataFolder , "metadata.json" ) ;
2019-09-02 18:03:57 +02:00
2020-01-12 04:01:04 +01:00
ApplicationMetadata appMetadata ;
2019-09-02 18:03:57 +02:00
2019-11-29 05:32:51 +01:00
if ( ! File . Exists ( metadataFile ) )
{
Directory . CreateDirectory ( metadataFolder ) ;
2019-09-02 18:03:57 +02:00
2020-01-12 04:01:04 +01:00
appMetadata = new ApplicationMetadata
2019-09-02 18:03:57 +02:00
{
2019-11-29 05:32:51 +01:00
Favorite = false ,
TimePlayed = 0 ,
LastPlayed = "Never"
} ;
2019-09-02 18:03:57 +02:00
2020-04-23 14:01:23 +02:00
using ( FileStream stream = File . Create ( metadataFile , 4096 , FileOptions . WriteThrough ) )
{
2020-04-30 14:07:41 +02:00
JsonHelper . Serialize ( stream , appMetadata , true ) ;
2020-04-23 14:01:23 +02:00
}
2019-09-02 18:03:57 +02:00
}
2019-11-29 05:32:51 +01:00
2020-04-30 14:07:41 +02:00
try
2019-09-02 18:03:57 +02:00
{
2020-04-30 14:07:41 +02:00
appMetadata = JsonHelper . DeserializeFromFile < ApplicationMetadata > ( metadataFile ) ;
}
catch ( JsonException )
{
Logger . PrintWarning ( LogClass . Application , $"Failed to parse metadata json for {titleId}. Loading defaults." ) ;
appMetadata = new ApplicationMetadata
2020-04-23 14:01:23 +02:00
{
2020-04-30 14:07:41 +02:00
Favorite = false ,
TimePlayed = 0 ,
LastPlayed = "Never"
} ;
2020-01-12 04:01:04 +01:00
}
if ( modifyFunction ! = null )
{
modifyFunction ( appMetadata ) ;
2020-04-23 14:01:23 +02:00
using ( FileStream stream = File . Create ( metadataFile , 4096 , FileOptions . WriteThrough ) )
{
2020-04-30 14:07:41 +02:00
JsonHelper . Serialize ( stream , appMetadata , true ) ;
2020-04-23 14:01:23 +02:00
}
2019-09-02 18:03:57 +02:00
}
2019-11-29 05:32:51 +01:00
2020-01-12 04:01:04 +01:00
return appMetadata ;
2019-09-02 18:03:57 +02:00
}
2019-11-29 05:32:51 +01:00
private static string ConvertSecondsToReadableString ( double seconds )
2019-09-02 18:03:57 +02:00
{
2019-11-29 05:32:51 +01:00
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 )
2019-09-02 18:03:57 +02:00
{
2019-11-29 05:32:51 +01:00
readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs" ;
2019-09-02 18:03:57 +02:00
}
else
{
2019-11-29 05:32:51 +01:00
readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days" ;
2019-09-02 18:03:57 +02:00
}
2019-11-29 05:32:51 +01:00
return readableString ;
2019-09-02 18:03:57 +02:00
}
2020-01-31 19:21:46 +01:00
2020-04-03 12:01:26 +02:00
private static void GetNameIdDeveloper ( ref ApplicationControlProperty controlData , out string titleName , out string titleId , out string publisher )
2020-01-31 19:21:46 +01:00
{
Enum . TryParse ( _desiredTitleLanguage . ToString ( ) , out TitleLanguage desiredTitleLanguage ) ;
2020-04-03 12:01:26 +02:00
if ( controlData . Titles . Length > ( int ) desiredTitleLanguage )
2020-01-31 19:21:46 +01:00
{
2020-04-03 12:01:26 +02:00
titleName = controlData . Titles [ ( int ) desiredTitleLanguage ] . Name . ToString ( ) ;
publisher = controlData . Titles [ ( int ) desiredTitleLanguage ] . Publisher . ToString ( ) ;
2020-01-31 19:21:46 +01:00
}
else
{
titleName = null ;
2020-04-03 12:01:26 +02:00
publisher = null ;
2020-01-31 19:21:46 +01:00
}
if ( string . IsNullOrWhiteSpace ( titleName ) )
{
2020-04-03 12:01:26 +02:00
foreach ( ApplicationControlTitle controlTitle in controlData . Titles )
{
if ( ! ( ( U8Span ) controlTitle . Name ) . IsEmpty ( ) )
{
titleName = controlTitle . Name . ToString ( ) ;
break ;
}
}
2020-01-31 19:21:46 +01:00
}
2020-04-03 12:01:26 +02:00
if ( string . IsNullOrWhiteSpace ( publisher ) )
2020-01-31 19:21:46 +01:00
{
2020-04-03 12:01:26 +02:00
foreach ( ApplicationControlTitle controlTitle in controlData . Titles )
{
if ( ! ( ( U8Span ) controlTitle . Publisher ) . IsEmpty ( ) )
{
publisher = controlTitle . Publisher . ToString ( ) ;
break ;
}
}
2020-01-31 19:21:46 +01:00
}
if ( controlData . PresenceGroupId ! = 0 )
{
titleId = controlData . PresenceGroupId . ToString ( "x16" ) ;
}
2020-04-03 12:01:26 +02:00
else if ( controlData . SaveDataOwnerId . Value ! = 0 )
2020-01-31 19:21:46 +01:00
{
2020-04-03 12:01:26 +02:00
titleId = controlData . SaveDataOwnerId . ToString ( ) ;
2020-01-31 19:21:46 +01:00
}
else if ( controlData . AddOnContentBaseId ! = 0 )
{
titleId = ( controlData . AddOnContentBaseId - 0x1000 ) . ToString ( "x16" ) ;
}
else
{
titleId = "0000000000000000" ;
}
}
2020-04-12 23:02:37 +02:00
private static bool IsUpdateApplied ( string titleId , out string version )
{
string jsonPath = Path . Combine ( _virtualFileSystem . GetBasePath ( ) , "games" , titleId , "updates.json" ) ;
if ( File . Exists ( jsonPath ) )
{
2020-04-30 14:07:41 +02:00
string updatePath = JsonHelper . DeserializeFromFile < TitleUpdateMetadata > ( jsonPath ) . Selected ;
if ( ! File . Exists ( updatePath ) )
2020-04-12 23:02:37 +02:00
{
2020-04-30 14:07:41 +02:00
version = "" ;
2020-04-12 23:02:37 +02:00
2020-04-30 14:07:41 +02:00
return false ;
}
2020-04-12 23:02:37 +02:00
2020-04-30 14:07:41 +02:00
using ( FileStream file = new FileStream ( updatePath , FileMode . Open , FileAccess . Read ) )
{
PartitionFileSystem nsp = new PartitionFileSystem ( file . AsStorage ( ) ) ;
2020-04-12 23:02:37 +02:00
2020-05-15 08:16:46 +02:00
_virtualFileSystem . ImportTickets ( nsp ) ;
2020-04-12 23:02:37 +02:00
2020-04-30 14:07:41 +02:00
foreach ( DirectoryEntryEx fileEntry in nsp . EnumerateEntries ( "/" , "*.nca" ) )
{
nsp . OpenFile ( out IFile ncaFile , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-04-12 23:02:37 +02:00
2020-05-03 01:43:22 +02:00
try
2020-04-30 14:07:41 +02:00
{
2020-05-03 01:43:22 +02:00
Nca nca = new Nca ( _virtualFileSystem . KeySet , ncaFile . AsStorage ( ) ) ;
2020-04-12 23:02:37 +02:00
2020-05-03 01:43:22 +02:00
if ( $"{nca.Header.TitleId.ToString(" x16 ")[..^3]}000" ! = titleId )
{
break ;
}
if ( nca . Header . ContentType = = NcaContentType . Control )
{
ApplicationControlProperty controlData = new ApplicationControlProperty ( ) ;
2020-04-12 23:02:37 +02:00
2020-05-03 01:43:22 +02:00
nca . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) . OpenFile ( out IFile nacpFile , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-04-12 23:02:37 +02:00
2020-05-03 01:43:22 +02:00
nacpFile . Read ( out _ , 0 , SpanHelpers . AsByteSpan ( ref controlData ) , ReadOption . None ) . ThrowIfFailure ( ) ;
2020-04-12 23:02:37 +02:00
2020-05-03 01:43:22 +02:00
version = controlData . DisplayVersion . ToString ( ) ;
2020-04-30 14:07:41 +02:00
2020-05-03 01:43:22 +02:00
return true ;
}
}
catch ( InvalidDataException )
{
Logger . PrintWarning ( LogClass . Application ,
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}" ) ;
break ;
}
catch ( MissingKeyException exception )
{
Logger . PrintWarning ( LogClass . Application ,
$"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}" ) ;
break ;
2020-04-12 23:02:37 +02:00
}
}
}
}
version = "" ;
return false ;
}
2019-09-02 18:03:57 +02:00
}
2020-04-12 23:02:37 +02:00
}