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-03-31 19:16:46 +00:00
using System.Globalization ;
2022-05-15 11:30:15 +00:00
using System.IO ;
2023-06-27 23:18:19 +00:00
using System.Linq ;
2022-05-15 11:30:15 +00:00
using System.Reflection ;
using System.Text ;
using System.Text.Json ;
using System.Threading ;
using Path = System . IO . Path ;
2023-06-29 00:39:22 +00:00
using TimeSpan = System . TimeSpan ;
2022-05-15 11:30:15 +00:00
namespace Ryujinx.Ui.App.Common
{
public class ApplicationLibrary
{
2023-06-29 00:39:22 +00:00
public event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
2022-05-15 11:30:15 +00:00
public event EventHandler < ApplicationCountUpdatedEventArgs > ApplicationCountUpdated ;
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 ;
2023-06-29 00:39:22 +00:00
private Language _desiredTitleLanguage ;
private CancellationTokenSource _cancellationToken ;
2022-05-15 11:30:15 +00:00
2023-06-29 00:39:22 +00:00
private static readonly ApplicationJsonSerializerContext _serializerContext = new ( JsonHelper . GetDefaultSerializerOptions ( ) ) ;
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new ( JsonHelper . GetDefaultSerializerOptions ( ) ) ;
2023-04-03 10:14:19 +00:00
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
{
2023-06-29 00:39:22 +00:00
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
2022-05-15 11:30:15 +00:00
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
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 )
{
2023-06-29 00:39:22 +00:00
int numApplicationsFound = 0 ;
2022-05-15 11:30:15 +00:00
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
2023-06-29 00:39:22 +00:00
( Path . GetExtension ( file ) . ToLower ( ) is ".nsp" & & ConfigurationState . Instance . Ui . ShownFileTypes . NSP . Value ) | |
2023-04-16 01:03:35 +00:00
( Path . GetExtension ( file ) . ToLower ( ) is ".pfs0" & & ConfigurationState . Instance . Ui . ShownFileTypes . PFS0 . Value ) | |
2023-06-29 00:39:22 +00:00
( 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 ) ;
2023-04-16 01:03:35 +00:00
} ) ;
2023-06-29 00:39:22 +00:00
2023-04-16 01:03:35 +00:00
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 ;
}
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
long fileSize = new FileInfo ( applicationPath ) . Length ;
2022-05-15 11:30:15 +00:00
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-10-22 23:30:46 +00:00
IFileSystem pfs ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
bool isExeFs = false ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( extension = = ".xci" )
{
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
pfs = xci . OpenPartition ( XciPartitionType . Secure ) ;
}
else
{
2023-10-22 23:30:46 +00:00
var pfsTemp = new PartitionFileSystem ( ) ;
pfsTemp . Initialize ( file . AsStorage ( ) ) . ThrowIfFailure ( ) ;
pfs = pfsTemp ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// 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
2023-06-01 16:24:00 +00:00
if ( iconSize > 0 )
{
applicationIcon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
}
else
{
applicationIcon = _nroIcon ;
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
// 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
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
// Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
if ( appMetadata . TimePlayedOld ! = default & & appMetadata . TimePlayed = = TimeSpan . Zero )
2023-01-15 23:11:16 +00:00
{
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
appMetadata . TimePlayed = TimeSpan . FromSeconds ( appMetadata . TimePlayedOld ) ;
appMetadata . TimePlayedOld = default ;
2023-05-11 23:56:37 +00:00
}
2022-05-15 11:30:15 +00:00
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
// Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
if ( appMetadata . LastPlayedOld ! = default & & ! appMetadata . LastPlayed . HasValue )
2023-05-11 23:56:37 +00:00
{
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
// Migrate from string-based last_played to DateTime-based last_played_utc.
if ( DateTime . TryParse ( appMetadata . LastPlayedOld , out DateTime lastPlayedOldParsed ) )
{
appMetadata . LastPlayed = lastPlayedOldParsed ;
// Migration successful: deleting last_played from the metadata file.
appMetadata . LastPlayedOld = default ;
}
2023-05-11 23:56:37 +00:00
2023-01-15 23:11:16 +00:00
}
2023-05-11 23:56:37 +00:00
} ) ;
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 ,
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
TimePlayed = appMetadata . TimePlayed ,
2022-05-15 11:30:15 +00:00
LastPlayed = appMetadata . LastPlayed ,
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize
Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils
* Additional ApplicationLibrary changes that got lost in the last commit
* Removed unneeded usings
* Removed converters as they are no longer needed
* Updated comment on FormatDateTime
* Removed base10 parameter from ValueFormatUtils
FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units
* Removed nullable attribute from TimePlayed property
Centralized TimePlayed update code into ApplicationMetadata
* Changed UpdateTimePlayed() to use TimeSpan logic
* Removed JsonIgnore attributes from ApplicationData
* Implemented requested format changes
* Fixed mistakes in method documentation comments
* Made it so the Last Played value "Never" is localized in the Avalonia UI
* Implemented suggestions
* Remove unused import
* Did a comment refinement pass in ValueFormatUtils.cs
* Reordered ValueFormatUtils methods and sorted them into #regions
* Integrated functionality from #5056
Also removed Logger print from last_played migration code
* Implemented suggestions
* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common
* common: Respect proper value format convention and use base10 by default
This could be discuss again in another issue/PR, for now revert to the previous behavior.
Signed-off-by: Mary Guillemard <mary@mary.zone>
---------
Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 21:47:44 +00:00
FileExtension = Path . GetExtension ( applicationPath ) . TrimStart ( '.' ) . ToUpper ( ) ,
FileSize = fileSize ,
2022-05-15 11:30:15 +00:00
Path = applicationPath ,
2023-06-29 00:39:22 +00:00
ControlHolder = controlHolder ,
2022-05-15 11:30:15 +00:00
} ;
numApplicationsLoaded + + ;
2023-06-29 00:39:22 +00:00
OnApplicationAdded ( new ApplicationAddedEventArgs
2022-05-15 11:30:15 +00:00
{
2023-06-29 00:39:22 +00:00
AppData = data ,
2022-05-15 11:30:15 +00:00
} ) ;
2023-06-29 00:39:22 +00:00
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs
2022-05-15 11:30:15 +00:00
{
NumAppsFound = numApplicationsFound ,
2023-06-29 00:39:22 +00:00
NumAppsLoaded = numApplicationsLoaded ,
2022-05-15 11:30:15 +00:00
} ) ;
}
2023-06-29 00:39:22 +00:00
OnApplicationCountUpdated ( new ApplicationCountUpdatedEventArgs
2022-05-15 11:30:15 +00:00
{
NumAppsFound = numApplicationsFound ,
2023-06-29 00:39:22 +00:00
NumAppsLoaded = numApplicationsLoaded ,
2022-05-15 11:30:15 +00:00
} ) ;
}
finally
{
_cancellationToken . Dispose ( ) ;
_cancellationToken = null ;
}
}
protected void OnApplicationAdded ( ApplicationAddedEventArgs e )
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
protected void OnApplicationCountUpdated ( ApplicationCountUpdatedEventArgs e )
{
ApplicationCountUpdated ? . Invoke ( null , e ) ;
}
2023-10-22 23:30:46 +00:00
private void GetControlFsAndTitleId ( IFileSystem pfs , out IFileSystem controlFs , out string titleId )
2022-05-15 11:30:15 +00:00
{
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 ) ;
2023-06-29 00:39:22 +00:00
titleId = controlNca ? . Header . TitleId . ToString ( "x16" ) ;
2022-05-15 11:30:15 +00:00
}
2023-06-29 00:39:22 +00:00
public static ApplicationMetadata LoadAndSaveMetaData ( string titleId , Action < ApplicationMetadata > modifyFunction = null )
2022-05-15 11:30:15 +00:00
{
string metadataFolder = Path . Combine ( AppDataManager . GamesDirPath , titleId , "gui" ) ;
2023-06-29 00:39:22 +00:00
string metadataFile = Path . Combine ( metadataFolder , "metadata.json" ) ;
2022-05-15 11:30:15 +00:00
ApplicationMetadata appMetadata ;
if ( ! File . Exists ( metadataFile ) )
{
Directory . CreateDirectory ( metadataFolder ) ;
appMetadata = new ApplicationMetadata ( ) ;
2023-06-29 00:39:22 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
try
{
2023-06-29 00:39:22 +00:00
appMetadata = JsonHelper . DeserializeFromFile ( metadataFile , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
catch ( JsonException )
{
Logger . Warning ? . Print ( LogClass . Application , $"Failed to parse metadata json for {titleId}. Loading defaults." ) ;
appMetadata = new ApplicationMetadata ( ) ;
}
if ( modifyFunction ! = null )
{
modifyFunction ( appMetadata ) ;
2023-06-29 00:39:22 +00:00
JsonHelper . SerializeToFile ( metadataFile , appMetadata , _serializerContext . ApplicationMetadata ) ;
2022-05-15 11:30:15 +00:00
}
return appMetadata ;
}
2023-10-20 18:51:15 +00:00
public byte [ ] GetApplicationIcon ( string applicationPath , Language desiredTitleLanguage )
2022-05-15 11:30:15 +00:00
{
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-10-22 23:30:46 +00:00
IFileSystem pfs ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
bool isExeFs = false ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( extension = = ".xci" )
{
Xci xci = new ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
pfs = xci . OpenPartition ( XciPartitionType . Secure ) ;
}
else
{
2023-10-22 23:30:46 +00:00
var pfsTemp = new PartitionFileSystem ( ) ;
pfsTemp . Initialize ( file . AsStorage ( ) ) . ThrowIfFailure ( ) ;
pfs = pfsTemp ;
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*" ) )
{
if ( Path . GetFileNameWithoutExtension ( fileEntry . FullPath ) = = "main" )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
isExeFs = true ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
}
2022-05-15 11:30:15 +00:00
2023-01-15 23:11:16 +00:00
if ( isExeFs )
{
applicationIcon = _nspIcon ;
}
else
{
// Store the ControlFS in variable called controlFs
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-10-20 18:51:15 +00:00
controlFs . OpenFile ( ref icon . Ref , $"/icon_{desiredTitleLanguage}.dat" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2023-01-15 23:11:16 +00:00
using MemoryStream stream = new ( ) ;
icon . Get . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
catch ( HorizonResultException )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
foreach ( DirectoryEntryEx entry in controlFs . EnumerateEntries ( "/" , "*" ) )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
if ( entry . Name = = "control.nacp" )
{
continue ;
}
2022-05-15 11:30:15 +00:00
using var icon = new UniqueRef < IFile > ( ) ;
2023-03-02 02:42:27 +00:00
controlFs . OpenFile ( ref icon . Ref , entry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 11:30:15 +00:00
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
2023-06-01 16:24:00 +00:00
if ( iconSize > 0 )
{
applicationIcon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
}
else
{
applicationIcon = _nroIcon ;
}
2022-05-15 11:30:15 +00:00
}
2023-01-15 23:11:16 +00:00
else
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
applicationIcon = _nroIcon ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
catch
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"The file encountered was not of a valid type. Errored File: {applicationPath}" ) ;
2022-05-15 11:30:15 +00:00
}
}
2023-01-15 23:11:16 +00:00
else if ( extension = = ".nca" )
{
applicationIcon = _ncaIcon ;
}
// If its an NSO we just set defaults
else if ( extension = = ".nso" )
{
applicationIcon = _nsoIcon ;
}
2022-05-15 11:30:15 +00:00
}
}
2023-06-29 00:39:22 +00:00
catch ( Exception )
2022-05-15 11:30:15 +00:00
{
2023-01-15 23:11:16 +00:00
Logger . Warning ? . Print ( LogClass . Application , $"Could not retrieve a valid icon for the app. Default icon will be used. Errored File: {applicationPath}" ) ;
2022-05-15 11:30:15 +00:00
}
return applicationIcon ? ? _ncaIcon ;
}
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
2023-10-22 23:30:46 +00:00
public static ( Nca main , Nca patch , Nca control ) GetGameData ( VirtualFileSystem fileSystem , IFileSystem pfs , int programIndex )
2023-03-31 19:16:46 +00:00
{
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 ( ) ;
2023-06-29 00:39:22 +00:00
Nca nca = new ( fileSystem . KeySet , ncaFile . Release ( ) . AsStorage ( ) ) ;
2023-03-31 19:16:46 +00:00
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 ( ) ;
2023-06-29 00:39:22 +00:00
Nca nca = new ( fileSystem . KeySet , ncaFile . Release ( ) . AsStorage ( ) ) ;
2023-03-31 19:16:46 +00:00
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-06-29 00:39:22 +00:00
updatePath = JsonHelper . DeserializeFromFile ( titleUpdateMetadataPath , _titleSerializerContext . TitleUpdateMetadata ) . Selected ;
2023-03-31 19:16:46 +00:00
if ( File . Exists ( updatePath ) )
{
2023-06-29 00:39:22 +00:00
FileStream file = new ( updatePath , FileMode . Open , FileAccess . Read ) ;
2023-10-22 23:30:46 +00:00
PartitionFileSystem nsp = new ( ) ;
nsp . Initialize ( file . AsStorage ( ) ) . ThrowIfFailure ( ) ;
2023-03-31 19:16:46 +00:00
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
}