2019-11-29 05:32:51 +01:00
using LibHac ;
2020-03-25 09:14:35 +01:00
using LibHac.Common ;
2022-01-12 12:22:19 +01:00
using LibHac.Common.Keys ;
2019-09-02 18:03:57 +02:00
using LibHac.Fs ;
2020-09-01 22:08:59 +02:00
using LibHac.Fs.Fsa ;
2019-10-17 08:17:44 +02:00
using LibHac.FsSystem ;
2020-03-25 18:09:38 +01:00
using LibHac.Ns ;
2022-01-12 12:22:19 +01:00
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
using LibHac.Tools.FsSystem.NcaUtils ;
2020-04-12 23:02:37 +02:00
using Ryujinx.Common.Configuration ;
2020-09-21 05:45:30 +02:00
using Ryujinx.Common.Logging ;
2020-01-21 23:23:11 +01:00
using Ryujinx.Configuration.System ;
2019-11-29 05:32:51 +01:00
using Ryujinx.HLE.FileSystem ;
2020-09-21 05:45:30 +02:00
using Ryujinx.HLE.HOS ;
2021-12-27 22:10:49 +01:00
using Ryujinx.HLE.HOS.SystemState ;
2019-11-29 05:32:51 +01:00
using Ryujinx.HLE.Loaders.Npdm ;
2019-09-02 18:03:57 +02:00
using System ;
using System.Collections.Generic ;
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-04-30 14:07:41 +02:00
using JsonHelper = Ryujinx . Common . Utilities . JsonHelper ;
2021-12-23 17:55:50 +01:00
using Path = System . IO . Path ;
2019-09-02 18:03:57 +02:00
2021-01-08 09:14:13 +01:00
namespace Ryujinx.Ui.App
2019-09-02 18:03:57 +02:00
{
public class ApplicationLibrary
{
2021-01-08 09:14:13 +01:00
public event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
public event EventHandler < ApplicationCountUpdatedEventArgs > ApplicationCountUpdated ;
2019-09-02 18:03:57 +02:00
2021-01-08 09:14:13 +01:00
private readonly byte [ ] _nspIcon ;
private readonly byte [ ] _xciIcon ;
private readonly byte [ ] _ncaIcon ;
private readonly byte [ ] _nroIcon ;
private readonly byte [ ] _nsoIcon ;
2019-09-02 18:03:57 +02:00
2021-01-08 09:14:13 +01:00
private VirtualFileSystem _virtualFileSystem ;
private Language _desiredTitleLanguage ;
2019-09-02 18:03:57 +02:00
2021-01-08 09:14:13 +01:00
public ApplicationLibrary ( VirtualFileSystem virtualFileSystem )
{
_virtualFileSystem = virtualFileSystem ;
_nspIcon = GetResourceBytes ( "Ryujinx.Ui.Resources.Icon_NSP.png" ) ;
_xciIcon = GetResourceBytes ( "Ryujinx.Ui.Resources.Icon_XCI.png" ) ;
_ncaIcon = GetResourceBytes ( "Ryujinx.Ui.Resources.Icon_NCA.png" ) ;
_nroIcon = GetResourceBytes ( "Ryujinx.Ui.Resources.Icon_NRO.png" ) ;
_nsoIcon = GetResourceBytes ( "Ryujinx.Ui.Resources.Icon_NSO.png" ) ;
}
private byte [ ] GetResourceBytes ( string resourceName )
{
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
resourceStream . Read ( resourceByteArray ) ;
return resourceByteArray ;
}
public IEnumerable < string > GetFilesInDirectory ( string directory )
2020-03-25 17:17:54 +01:00
{
Stack < string > stack = new Stack < string > ( ) ;
2020-09-21 05:45:30 +02:00
2020-03-25 17:17:54 +01:00
stack . Push ( directory ) ;
2020-09-21 05:45:30 +02:00
2020-03-25 17:17:54 +01:00
while ( stack . Count > 0 )
{
2020-09-21 05:45:30 +02:00
string dir = stack . Pop ( ) ;
2021-01-08 09:14:13 +01:00
string [ ] content = Array . Empty < string > ( ) ;
2020-03-25 17:17:54 +01:00
try
{
content = Directory . GetFiles ( dir , "*" ) ;
}
catch ( UnauthorizedAccessException )
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"Failed to get access to directory: \" { dir } \ "" ) ;
2020-03-25 17:17:54 +01:00
}
if ( content . Length > 0 )
{
foreach ( string file in content )
2020-09-21 05:45:30 +02:00
{
2020-03-25 17:17:54 +01:00
yield return file ;
2020-09-21 05:45:30 +02:00
}
2020-03-25 17:17:54 +01:00
}
try
{
content = Directory . GetDirectories ( dir ) ;
}
catch ( UnauthorizedAccessException )
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"Failed to get access to directory: \" { dir } \ "" ) ;
2020-03-25 17:17:54 +01:00
}
if ( content . Length > 0 )
{
foreach ( string subdir in content )
2020-09-21 05:45:30 +02:00
{
2020-03-25 17:17:54 +01:00
stack . Push ( subdir ) ;
2020-09-21 05:45:30 +02:00
}
2020-03-25 17:17:54 +01:00
}
}
}
2021-01-08 09:14:13 +01:00
public void ReadControlData ( IFileSystem controlFs , Span < byte > outProperty )
2020-03-25 18:09:38 +01:00
{
2021-12-23 17:55:50 +01:00
using var controlFile = new UniqueRef < IFile > ( ) ;
controlFs . OpenFile ( ref controlFile . Ref ( ) , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
controlFile . Get . Read ( out _ , 0 , outProperty , ReadOption . None ) . ThrowIfFailure ( ) ;
2020-03-25 18:09:38 +01:00
}
2021-01-08 09:14:13 +01:00
public void LoadApplications ( List < string > appDirs , 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
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 > ( ) ;
2020-09-21 05:45:30 +02:00
2019-11-29 05:32:51 +01:00
foreach ( string appDir in appDirs )
2019-09-02 18:03:57 +02:00
{
2021-12-27 22:10:49 +01:00
2020-01-31 19:21:46 +01:00
if ( ! Directory . Exists ( appDir ) )
2019-09-02 18:03:57 +02:00
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"The \" game_dirs \ " section in \"Config.json\" contains an invalid directory: \"{appDir}\"" ) ;
2019-09-02 18:03:57 +02:00
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" ;
2019-09-02 18:03:57 +02:00
byte [ ] applicationIcon = null ;
2020-09-21 05:45:30 +02:00
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
{
2021-12-23 17:55:50 +01:00
using var ncaFile = new UniqueRef < IFile > ( ) ;
pfs . OpenFile ( ref ncaFile . Ref ( ) , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-01-31 19:21:46 +01:00
2021-12-23 17:55:50 +01:00
Nca nca = new Nca ( _virtualFileSystem . KeySet , ncaFile . Get . AsStorage ( ) ) ;
2020-02-15 21:20:19 +01:00
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2021-12-23 17:55:50 +01: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 ( ) ) )
2020-02-15 21:20:19 +01:00
{
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
2021-12-23 17:55:50 +01:00
using var npdmFile = new UniqueRef < IFile > ( ) ;
Result result = pfs . OpenFile ( ref npdmFile . Ref ( ) , "/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
{
2021-12-23 17:55:50 +01:00
Npdm npdm = new Npdm ( npdmFile . Get . 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
{
GetControlFsAndTitleId ( pfs , out IFileSystem controlFs , out titleId ) ;
2019-09-02 18:03:57 +02:00
2022-02-22 14:53:39 +01:00
// Check if there is an update available.
if ( IsUpdateApplied ( titleId , out IFileSystem updatedControlFs ) )
{
// Replace the original ControlFs by the updated one.
controlFs = updatedControlFs ;
}
2020-03-25 18:09:38 +01:00
2022-02-22 14:53:39 +01:00
ReadControlData ( controlFs , controlHolder . ByteSpan ) ;
2019-09-02 18:03:57 +02:00
2022-02-22 14:53:39 +01:00
GetGameInformation ( ref controlHolder . Value , out titleName , out _ , out developer , out version ) ;
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
{
2021-12-23 17:55:50 +01:00
using var icon = new UniqueRef < IFile > ( ) ;
controlFs . OpenFile ( ref icon . Ref ( ) , $"/icon_{_desiredTitleLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-11-29 05:32:51 +01:00
using ( MemoryStream stream = new MemoryStream ( ) )
{
2021-12-23 17:55:50 +01:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
2019-11-29 05:32:51 +01:00
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 ;
}
2021-12-23 17:55:50 +01:00
using var icon = new UniqueRef < IFile > ( ) ;
controlFs . OpenFile ( ref icon . Ref ( ) , entry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-02-15 21:20:19 +01:00
using ( MemoryStream stream = new MemoryStream ( ) )
{
2021-12-23 17:55:50 +01:00
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
2020-02-15 21:20:19 +01:00
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-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
2020-02-15 21:20:19 +01:00
}
catch ( InvalidDataException )
{
applicationIcon = Path . GetExtension ( applicationPath ) . ToLower ( ) = = ".xci" ? _xciIcon : _nspIcon ;
2020-01-31 19:21:46 +01:00
2020-08-04 01:32:53 +02: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}" ) ;
2020-02-15 21:20:19 +01:00
}
catch ( Exception exception )
{
2021-01-26 18:45:07 +01:00
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}" ) ;
2019-09-02 18:03:57 +02:00
2020-02-15 21:20:19 +01:00
numApplicationsFound - - ;
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
2022-02-22 14:53:39 +01:00
GetGameInformation ( ref controlHolder . Value , out titleName , out titleId , out developer , out version ) ;
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-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( 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
2021-12-23 17:55:50 +01:00
if ( nca . Header . ContentType ! = NcaContentType . Program | | ( nca . SectionExists ( NcaSectionType . Data ) & & nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) ) )
2020-02-15 21:20:19 +01:00
{
numApplicationsFound - - ;
continue ;
}
}
catch ( InvalidDataException )
2020-01-31 19:21:46 +01:00
{
2020-08-04 01:32:53 +02: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}" ) ;
2020-02-15 21:20:19 +01:00
}
catch
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
2020-02-15 21:20:19 +01:00
2020-01-31 19:21:46 +01:00
numApplicationsFound - - ;
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 )
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , exception . Message ) ;
2020-01-31 19:21:46 +01: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 ;
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-11-27 19:05:36 +01:00
if ( appMetadata . LastPlayed ! = "Never" & & ! 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?)" ) ;
appMetadata . LastPlayed = "Never" ;
}
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
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
} ) ;
2019-09-02 18:03:57 +02:00
}
2021-01-08 09:14:13 +01:00
protected void OnApplicationAdded ( ApplicationAddedEventArgs e )
2019-11-29 05:32:51 +01:00
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
2021-01-08 09:14:13 +01:00
protected void OnApplicationCountUpdated ( ApplicationCountUpdatedEventArgs e )
2020-01-31 19:21:46 +01:00
{
ApplicationCountUpdated ? . Invoke ( null , e ) ;
}
2021-01-08 09:14:13 +01:00
private void GetControlFsAndTitleId ( PartitionFileSystem pfs , out IFileSystem controlFs , out string titleId )
2019-09-02 18:03:57 +02:00
{
2020-09-21 05:45:30 +02:00
( _ , _ , Nca controlNca ) = ApplicationLoader . GetGameData ( _virtualFileSystem , pfs , 0 ) ;
2019-09-02 18:03:57 +02:00
// Return the ControlFS
2020-02-11 23:43:24 +01:00
controlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
2021-01-08 09:14:13 +01:00
titleId = controlNca ? . Header . TitleId . ToString ( "x16" ) ;
2019-09-02 18:03:57 +02:00
}
2021-01-08 09:14:13 +01:00
internal ApplicationMetadata LoadAndSaveMetaData ( string titleId , Action < ApplicationMetadata > modifyFunction = null )
2019-09-02 18:03:57 +02:00
{
2020-08-30 18:51:53 +02:00
string metadataFolder = Path . Combine ( AppDataManager . GamesDirPath , 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
2021-01-08 09:14:13 +01:00
appMetadata = new ApplicationMetadata ( ) ;
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 )
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"Failed to parse metadata json for {titleId}. Loading defaults." ) ;
2020-04-30 14:07:41 +02:00
2021-01-08 09:14:13 +01:00
appMetadata = new ApplicationMetadata ( ) ;
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
}
2021-01-08 09:14:13 +01:00
private 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
2022-02-22 14:53:39 +01:00
private void GetGameInformation ( ref ApplicationControlProperty controlData , out string titleName , out string titleId , out string publisher , out string version )
2020-01-31 19:21:46 +01:00
{
2021-12-27 22:10:49 +01:00
_ = Enum . TryParse ( _desiredTitleLanguage . ToString ( ) , out TitleLanguage desiredTitleLanguage ) ;
2020-01-31 19:21:46 +01:00
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 ( ) ;
2020-09-21 05:45:30 +02:00
2020-04-03 12:01:26 +02:00
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 ( ) ;
2020-09-21 05:45:30 +02:00
2020-04-03 12:01:26 +02:00
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" ;
}
2022-02-22 14:53:39 +01:00
version = controlData . DisplayVersion . ToString ( ) ;
2020-01-31 19:21:46 +01:00
}
2020-04-12 23:02:37 +02:00
2022-02-22 14:53:39 +01:00
private bool IsUpdateApplied ( string titleId , out IFileSystem updatedControlFs )
2020-04-12 23:02:37 +02:00
{
2022-02-22 14:53:39 +01:00
updatedControlFs = null ;
2020-09-21 05:45:30 +02:00
string updatePath = "(unknown)" ;
2020-04-12 23:02:37 +02:00
2020-09-21 05:45:30 +02:00
try
2020-04-12 23:02:37 +02:00
{
2020-09-21 05:45:30 +02:00
( Nca patchNca , Nca controlNca ) = ApplicationLoader . GetGameUpdateData ( _virtualFileSystem , titleId , 0 , out updatePath ) ;
2020-04-12 23:02:37 +02:00
2020-09-21 05:45:30 +02:00
if ( patchNca ! = null & & controlNca ! = null )
2020-04-30 14:07:41 +02:00
{
2022-02-22 14:53:39 +01:00
updatedControlFs = controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
2020-05-03 01:43:22 +02:00
2020-09-21 05:45:30 +02:00
return true ;
2020-04-12 23:02:37 +02:00
}
}
2020-09-21 05:45:30 +02:00
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: {updatePath}" ) ;
}
catch ( MissingKeyException exception )
{
Logger . Warning ? . Print ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}" ) ;
}
2020-04-12 23:02:37 +02:00
return false ;
}
2019-09-02 18:03:57 +02:00
}
2020-04-12 23:02:37 +02:00
}