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