2020-06-16 20:28:02 +02:00
using ARMeilleure.Translation.PTC ;
2020-05-15 08:16:46 +02:00
using LibHac ;
using LibHac.Account ;
using LibHac.Common ;
using LibHac.Fs ;
2020-09-01 22:08:59 +02:00
using LibHac.Fs.Fsa ;
2021-08-12 23:56:24 +02:00
using LibHac.Fs.Shim ;
2020-05-15 08:16:46 +02:00
using LibHac.FsSystem ;
2021-08-12 23:56:24 +02:00
using LibHac.Loader ;
using LibHac.Ncm ;
2020-05-15 08:16:46 +02: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-05-15 08:16:46 +02:00
using Ryujinx.Common.Configuration ;
using Ryujinx.Common.Logging ;
using Ryujinx.HLE.FileSystem ;
2021-03-27 15:12:05 +01:00
using Ryujinx.HLE.HOS.Kernel.Process ;
2020-05-15 08:16:46 +02:00
using Ryujinx.HLE.Loaders.Executables ;
2022-05-03 01:30:02 +02:00
using Ryujinx.Memory ;
2020-05-15 08:16:46 +02:00
using System ;
using System.Collections.Generic ;
2020-09-21 05:45:30 +02:00
using System.Globalization ;
2020-05-15 08:16:46 +02:00
using System.IO ;
using System.Linq ;
using System.Reflection ;
2022-05-05 20:23:30 +02:00
using System.Text ;
2020-12-29 20:54:32 +01:00
using static Ryujinx . HLE . HOS . ModLoader ;
2020-09-01 22:08:59 +02:00
using ApplicationId = LibHac . Ncm . ApplicationId ;
2021-12-23 17:55:50 +01:00
using Path = System . IO . Path ;
2020-05-15 08:16:46 +02:00
namespace Ryujinx.HLE.HOS
{
using JsonHelper = Common . Utilities . JsonHelper ;
public class ApplicationLoader
{
2020-09-21 05:45:30 +02:00
// Binaries from exefs are loaded into mem in this order. Do not change.
2020-12-29 20:54:32 +01:00
internal static readonly string [ ] ExeFsPrefixes =
{
"rtld" ,
"main" ,
"subsdk0" ,
"subsdk1" ,
"subsdk2" ,
"subsdk3" ,
"subsdk4" ,
"subsdk5" ,
"subsdk6" ,
"subsdk7" ,
"subsdk8" ,
"subsdk9" ,
"sdk"
} ;
2020-05-15 08:16:46 +02:00
2021-05-16 17:12:14 +02:00
private readonly Switch _device ;
2020-09-21 05:45:30 +02:00
private string _titleName ;
private string _displayVersion ;
private BlitStruct < ApplicationControlProperty > _controlData ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
public BlitStruct < ApplicationControlProperty > ControlData = > _controlData ;
public string TitleName = > _titleName ;
public string DisplayVersion = > _displayVersion ;
2020-05-15 08:16:46 +02:00
2021-08-12 23:56:24 +02:00
public ulong TitleId { get ; private set ; }
public bool TitleIs64Bit { get ; private set ; }
2020-06-16 20:28:02 +02:00
2020-09-21 05:45:30 +02:00
public string TitleIdText = > TitleId . ToString ( "x16" ) ;
2020-05-15 08:16:46 +02:00
2021-05-16 17:12:14 +02:00
public ApplicationLoader ( Switch device )
2020-05-15 08:16:46 +02:00
{
2021-08-12 23:56:24 +02:00
_device = device ;
2020-09-21 05:45:30 +02:00
_controlData = new BlitStruct < ApplicationControlProperty > ( 1 ) ;
2020-05-15 08:16:46 +02:00
}
public void LoadCart ( string exeFsDir , string romFsFile = null )
{
if ( romFsFile ! = null )
{
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . LoadRomFs ( romFsFile ) ;
2020-05-15 08:16:46 +02:00
}
LocalFileSystem codeFs = new LocalFileSystem ( exeFsDir ) ;
2021-08-12 23:56:24 +02:00
MetaLoader metaData = ReadNpdm ( codeFs ) ;
2020-05-15 08:16:46 +02:00
2022-03-06 22:12:01 +01:00
_device . Configuration . VirtualFileSystem . ModLoader . CollectMods (
2022-05-03 23:28:32 +02:00
new [ ] { TitleId } ,
_device . Configuration . VirtualFileSystem . ModLoader . GetModsBasePath ( ) ,
2022-03-06 22:12:01 +01:00
_device . Configuration . VirtualFileSystem . ModLoader . GetSdModsBasePath ( ) ) ;
2020-07-15 01:40:17 +02:00
2020-05-15 08:16:46 +02:00
if ( TitleId ! = 0 )
{
2020-09-01 22:08:59 +02:00
EnsureSaveData ( new ApplicationId ( TitleId ) ) ;
2020-05-15 08:16:46 +02:00
}
2020-07-09 06:31:15 +02:00
LoadExeFs ( codeFs , metaData ) ;
2020-05-15 08:16:46 +02:00
}
2020-09-21 05:45:30 +02:00
public static ( Nca main , Nca patch , Nca control ) GetGameData ( VirtualFileSystem fileSystem , PartitionFileSystem pfs , int programIndex )
2020-05-15 08:16:46 +02:00
{
2021-08-12 23:56:24 +02:00
Nca mainNca = null ;
Nca patchNca = null ;
2020-05-15 08:16:46 +02:00
Nca controlNca = null ;
2020-09-21 05:45:30 +02:00
fileSystem . ImportTickets ( pfs ) ;
2020-05-15 08:16:46 +02:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*.nca" ) )
{
2021-12-23 17:55:50 +01:00
using var ncaFile = new UniqueRef < IFile > ( ) ;
2020-05-15 08:16:46 +02:00
2021-12-23 17:55:50 +01:00
pfs . OpenFile ( ref ncaFile . Ref ( ) , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
Nca nca = new Nca ( fileSystem . KeySet , ncaFile . Release ( ) . AsStorage ( ) ) ;
2020-09-21 05:45:30 +02:00
int ncaProgramIndex = ( int ) ( nca . Header . TitleId & 0xF ) ;
if ( ncaProgramIndex ! = programIndex )
{
continue ;
}
2020-05-15 08:16:46 +02:00
if ( nca . Header . ContentType = = NcaContentType . Program )
{
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2021-12-23 17:55:50 +01:00
if ( nca . SectionExists ( NcaSectionType . Data ) & & nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
2020-05-15 08:16:46 +02:00
{
patchNca = nca ;
}
else
{
mainNca = nca ;
}
}
else if ( nca . Header . ContentType = = NcaContentType . Control )
{
controlNca = nca ;
}
}
return ( mainNca , patchNca , controlNca ) ;
}
2020-09-21 05:45:30 +02:00
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" ) )
{
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-09-21 05:45:30 +02:00
2021-12-23 17:55:50 +01:00
Nca nca = new Nca ( fileSystem . KeySet , ncaFile . Release ( ) . AsStorage ( ) ) ;
2020-09-21 05:45:30 +02: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 & = 0xFFFFFFFFFFFFFFF0 ;
// Load update informations if existing.
string titleUpdateMetadataPath = Path . Combine ( AppDataManager . GamesDirPath , titleIdBase . ToString ( "x16" ) , "updates.json" ) ;
if ( File . Exists ( titleUpdateMetadataPath ) )
{
updatePath = JsonHelper . DeserializeFromFile < TitleUpdateMetadata > ( titleUpdateMetadataPath ) . Selected ;
if ( File . Exists ( updatePath ) )
{
FileStream file = new FileStream ( updatePath , FileMode . Open , FileAccess . Read ) ;
PartitionFileSystem nsp = new PartitionFileSystem ( file . AsStorage ( ) ) ;
return GetGameUpdateDataFromPartition ( fileSystem , nsp , titleIdBase . ToString ( "x16" ) , programIndex ) ;
}
}
}
return ( null , null ) ;
}
2020-05-15 08:16:46 +02:00
public void LoadXci ( string xciFile )
{
FileStream file = new FileStream ( xciFile , FileMode . Open , FileAccess . Read ) ;
2021-08-12 23:56:24 +02:00
Xci xci = new Xci ( _device . Configuration . VirtualFileSystem . KeySet , file . AsStorage ( ) ) ;
2020-05-15 08:16:46 +02:00
if ( ! xci . HasPartition ( XciPartitionType . Secure ) )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , "Unable to load XCI: Could not find XCI secure partition" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
PartitionFileSystem securePartition = xci . OpenPartition ( XciPartitionType . Secure ) ;
2020-09-21 05:45:30 +02:00
Nca mainNca ;
Nca patchNca ;
Nca controlNca ;
2020-05-15 08:16:46 +02:00
try
{
2021-05-16 17:12:14 +02:00
( mainNca , patchNca , controlNca ) = GetGameData ( _device . Configuration . VirtualFileSystem , securePartition , _device . Configuration . UserChannelPersistence . Index ) ;
2021-08-12 23:56:24 +02:00
RegisterProgramMapInfo ( securePartition ) . ThrowIfFailure ( ) ;
2020-05-15 08:16:46 +02:00
}
catch ( Exception e )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , $"Unable to load XCI: {e.Message}" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
if ( mainNca = = null )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , "Unable to load XCI: Could not find Main NCA" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
2021-05-16 17:12:14 +02:00
_device . Configuration . ContentManager . LoadEntries ( _device ) ;
_device . Configuration . ContentManager . ClearAocData ( ) ;
_device . Configuration . ContentManager . AddAocData ( securePartition , xciFile , mainNca . Header . TitleId , _device . Configuration . FsIntegrityCheckLevel ) ;
2020-06-20 19:38:14 +02:00
2020-05-15 08:16:46 +02:00
LoadNca ( mainNca , patchNca , controlNca ) ;
}
public void LoadNsp ( string nspFile )
{
2021-08-12 23:56:24 +02:00
FileStream file = new FileStream ( nspFile , FileMode . Open , FileAccess . Read ) ;
PartitionFileSystem nsp = new PartitionFileSystem ( file . AsStorage ( ) ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
Nca mainNca ;
Nca patchNca ;
Nca controlNca ;
2020-05-15 08:16:46 +02:00
try
{
2021-05-16 17:12:14 +02:00
( mainNca , patchNca , controlNca ) = GetGameData ( _device . Configuration . VirtualFileSystem , nsp , _device . Configuration . UserChannelPersistence . Index ) ;
2021-08-12 23:56:24 +02:00
RegisterProgramMapInfo ( nsp ) . ThrowIfFailure ( ) ;
2020-05-15 08:16:46 +02:00
}
catch ( Exception e )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , $"Unable to load NSP: {e.Message}" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
if ( mainNca ! = null )
{
2021-05-16 17:12:14 +02:00
_device . Configuration . ContentManager . ClearAocData ( ) ;
_device . Configuration . ContentManager . AddAocData ( nsp , nspFile , mainNca . Header . TitleId , _device . Configuration . FsIntegrityCheckLevel ) ;
2020-06-20 19:38:14 +02:00
2020-05-15 08:16:46 +02:00
LoadNca ( mainNca , patchNca , controlNca ) ;
return ;
}
// This is not a normal NSP, it's actually a ExeFS as a NSP
2022-05-31 21:16:59 +02:00
LoadExeFs ( nsp , null , isHomebrew : true ) ;
2020-05-15 08:16:46 +02:00
}
public void LoadNca ( string ncaFile )
{
FileStream file = new FileStream ( ncaFile , FileMode . Open , FileAccess . Read ) ;
2021-08-12 23:56:24 +02:00
Nca nca = new Nca ( _device . Configuration . VirtualFileSystem . KeySet , file . AsStorage ( false ) ) ;
2020-05-15 08:16:46 +02:00
LoadNca ( nca , null , null ) ;
}
2022-05-05 20:23:30 +02:00
public void LoadServiceNca ( string ncaFile )
{
// Disable PPTC here as it does not support multiple processes running.
// TODO: This should be eventually removed and it should stop using global state and
// instead manage the cache per process.
Ptc . Close ( ) ;
PtcProfiler . Stop ( ) ;
FileStream file = new FileStream ( ncaFile , FileMode . Open , FileAccess . Read ) ;
Nca mainNca = new Nca ( _device . Configuration . VirtualFileSystem . KeySet , file . AsStorage ( false ) ) ;
if ( mainNca . Header . ContentType ! = NcaContentType . Program )
{
Logger . Error ? . Print ( LogClass . Loader , "Selected NCA is not a \"Program\" NCA" ) ;
return ;
}
IFileSystem codeFs = null ;
if ( mainNca . CanOpenSection ( NcaSectionType . Code ) )
{
codeFs = mainNca . OpenFileSystem ( NcaSectionType . Code , _device . System . FsIntegrityCheckLevel ) ;
}
if ( codeFs = = null )
{
Logger . Error ? . Print ( LogClass . Loader , "No ExeFS found in NCA" ) ;
return ;
}
using var npdmFile = new UniqueRef < IFile > ( ) ;
Result result = codeFs . OpenFile ( ref npdmFile . Ref ( ) , "/main.npdm" . ToU8Span ( ) , OpenMode . Read ) ;
MetaLoader metaData ;
npdmFile . Get . GetSize ( out long fileSize ) . ThrowIfFailure ( ) ;
var npdmBuffer = new byte [ fileSize ] ;
npdmFile . Get . Read ( out _ , 0 , npdmBuffer ) . ThrowIfFailure ( ) ;
metaData = new MetaLoader ( ) ;
metaData . Load ( npdmBuffer ) . ThrowIfFailure ( ) ;
NsoExecutable [ ] nsos = new NsoExecutable [ ExeFsPrefixes . Length ] ;
for ( int i = 0 ; i < nsos . Length ; i + + )
{
string name = ExeFsPrefixes [ i ] ;
if ( ! codeFs . FileExists ( $"/{name}" ) )
{
continue ; // File doesn't exist, skip.
}
Logger . Info ? . Print ( LogClass . Loader , $"Loading {name}..." ) ;
using var nsoFile = new UniqueRef < IFile > ( ) ;
codeFs . OpenFile ( ref nsoFile . Ref ( ) , $"/{name}" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
nsos [ i ] = new NsoExecutable ( nsoFile . Release ( ) . AsStorage ( ) , name ) ;
}
// Collect the nsos, ignoring ones that aren't used.
NsoExecutable [ ] programs = nsos . Where ( x = > x ! = null ) . ToArray ( ) ;
MemoryManagerMode memoryManagerMode = _device . Configuration . MemoryManagerMode ;
if ( ! MemoryBlock . SupportsFlags ( MemoryAllocationFlags . ViewCompatible ) )
{
memoryManagerMode = MemoryManagerMode . SoftwarePageTable ;
}
metaData . GetNpdm ( out Npdm npdm ) . ThrowIfFailure ( ) ;
ProgramInfo programInfo = new ProgramInfo ( in npdm , allowCodeMemoryForJit : false ) ;
ProgramLoader . LoadNsos ( _device . System . KernelContext , out _ , metaData , programInfo , executables : programs ) ;
string titleIdText = npdm . Aci . Value . ProgramId . Value . ToString ( "x16" ) ;
bool titleIs64Bit = ( npdm . Meta . Value . Flags & 1 ) ! = 0 ;
string programName = Encoding . ASCII . GetString ( npdm . Meta . Value . ProgramName ) . TrimEnd ( '\0' ) ;
Logger . Info ? . Print ( LogClass . Loader , $"Service Loaded: {programName} [{titleIdText}] [{(titleIs64Bit ? " 64 - bit " : " 32 - bit ")}]" ) ;
}
2020-05-15 08:16:46 +02:00
private void LoadNca ( Nca mainNca , Nca patchNca , Nca controlNca )
{
if ( mainNca . Header . ContentType ! = NcaContentType . Program )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , "Selected NCA is not a \"Program\" NCA" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
2021-08-12 23:56:24 +02:00
IStorage dataStorage = null ;
IFileSystem codeFs = null ;
2020-05-15 08:16:46 +02:00
2021-05-16 17:12:14 +02:00
( Nca updatePatchNca , Nca updateControlNca ) = GetGameUpdateData ( _device . Configuration . VirtualFileSystem , mainNca . Header . TitleId . ToString ( "x16" ) , _device . Configuration . UserChannelPersistence . Index , out _ ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( updatePatchNca ! = null )
2020-05-15 08:16:46 +02:00
{
2020-09-21 05:45:30 +02:00
patchNca = updatePatchNca ;
}
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( updateControlNca ! = null )
{
controlNca = updateControlNca ;
2020-05-15 08:16:46 +02:00
}
2020-09-21 05:45:30 +02:00
// Load program 0 control NCA as we are going to need it for display version.
2021-05-16 17:12:14 +02:00
( _ , Nca updateProgram0ControlNca ) = GetGameUpdateData ( _device . Configuration . VirtualFileSystem , mainNca . Header . TitleId . ToString ( "x16" ) , 0 , out _ ) ;
2020-09-21 05:45:30 +02:00
2020-06-23 02:32:07 +02:00
// Load Aoc
2020-08-30 18:51:53 +02:00
string titleAocMetadataPath = Path . Combine ( AppDataManager . GamesDirPath , mainNca . Header . TitleId . ToString ( "x16" ) , "dlc.json" ) ;
2020-06-23 02:32:07 +02:00
if ( File . Exists ( titleAocMetadataPath ) )
{
2022-07-29 00:41:34 +02:00
List < DownloadableContentContainer > dlcContainerList = JsonHelper . DeserializeFromFile < List < DownloadableContentContainer > > ( titleAocMetadataPath ) ;
2020-06-23 02:32:07 +02:00
2022-07-29 00:41:34 +02:00
foreach ( DownloadableContentContainer downloadableContentContainer in dlcContainerList )
2020-06-23 02:32:07 +02:00
{
2022-07-29 00:41:34 +02:00
foreach ( DownloadableContentNca downloadableContentNca in downloadableContentContainer . DownloadableContentNcaList )
2020-06-23 02:32:07 +02:00
{
2022-07-29 00:41:34 +02:00
if ( File . Exists ( downloadableContentContainer . ContainerPath ) )
2021-11-29 19:21:27 +01:00
{
2022-07-29 00:41:34 +02:00
_device . Configuration . ContentManager . AddAocItem ( downloadableContentNca . TitleId , downloadableContentContainer . ContainerPath , downloadableContentNca . FullPath , downloadableContentNca . Enabled ) ;
2021-11-29 19:21:27 +01:00
}
else
{
2022-07-29 00:41:34 +02:00
Logger . Warning ? . Print ( LogClass . Application , $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed." ) ;
2021-11-29 19:21:27 +01:00
}
2020-06-23 02:32:07 +02:00
}
}
}
2020-05-15 08:16:46 +02:00
if ( patchNca = = null )
{
if ( mainNca . CanOpenSection ( NcaSectionType . Data ) )
{
2020-07-09 06:31:15 +02:00
dataStorage = mainNca . OpenStorage ( NcaSectionType . Data , _device . System . FsIntegrityCheckLevel ) ;
2020-05-15 08:16:46 +02:00
}
if ( mainNca . CanOpenSection ( NcaSectionType . Code ) )
{
2020-07-09 06:31:15 +02:00
codeFs = mainNca . OpenFileSystem ( NcaSectionType . Code , _device . System . FsIntegrityCheckLevel ) ;
2020-05-15 08:16:46 +02:00
}
}
else
{
if ( patchNca . CanOpenSection ( NcaSectionType . Data ) )
{
2020-07-09 06:31:15 +02:00
dataStorage = mainNca . OpenStorageWithPatch ( patchNca , NcaSectionType . Data , _device . System . FsIntegrityCheckLevel ) ;
2020-05-15 08:16:46 +02:00
}
if ( patchNca . CanOpenSection ( NcaSectionType . Code ) )
{
2020-07-09 06:31:15 +02:00
codeFs = mainNca . OpenFileSystemWithPatch ( patchNca , NcaSectionType . Code , _device . System . FsIntegrityCheckLevel ) ;
2020-05-15 08:16:46 +02:00
}
}
if ( codeFs = = null )
{
2020-08-04 01:32:53 +02:00
Logger . Error ? . Print ( LogClass . Loader , "No ExeFS found in NCA" ) ;
2020-05-15 08:16:46 +02:00
return ;
}
2021-08-12 23:56:24 +02:00
MetaLoader metaData = ReadNpdm ( codeFs ) ;
2020-07-09 06:31:15 +02:00
2022-03-06 22:12:01 +01:00
_device . Configuration . VirtualFileSystem . ModLoader . CollectMods (
2022-05-03 23:28:32 +02:00
_device . Configuration . ContentManager . GetAocTitleIds ( ) . Prepend ( TitleId ) ,
_device . Configuration . VirtualFileSystem . ModLoader . GetModsBasePath ( ) ,
2022-03-06 22:12:01 +01:00
_device . Configuration . VirtualFileSystem . ModLoader . GetSdModsBasePath ( ) ) ;
2020-07-09 06:31:15 +02:00
if ( controlNca ! = null )
2020-05-15 08:16:46 +02:00
{
2020-09-21 05:45:30 +02:00
ReadControlData ( _device , controlNca , ref _controlData , ref _titleName , ref _displayVersion ) ;
2020-05-15 08:16:46 +02:00
}
else
{
2020-07-09 06:31:15 +02:00
ControlData . ByteSpan . Clear ( ) ;
2020-05-15 08:16:46 +02:00
}
2020-09-21 05:45:30 +02:00
// NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
// BODY: As such, to avoid PTC cache confusion, we only trust the the program 0 display version when launching a sub program.
2021-05-16 17:12:14 +02:00
if ( updateProgram0ControlNca ! = null & & _device . Configuration . UserChannelPersistence . Index ! = 0 )
2020-09-21 05:45:30 +02:00
{
string dummyTitleName = "" ;
BlitStruct < ApplicationControlProperty > dummyControl = new BlitStruct < ApplicationControlProperty > ( 1 ) ;
ReadControlData ( _device , updateProgram0ControlNca , ref dummyControl , ref dummyTitleName , ref _displayVersion ) ;
}
2020-07-09 06:31:15 +02:00
if ( dataStorage = = null )
2020-05-15 08:16:46 +02:00
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Loader , "No RomFS found in NCA" ) ;
2020-05-15 08:16:46 +02:00
}
else
{
2021-05-16 17:12:14 +02:00
IStorage newStorage = _device . Configuration . VirtualFileSystem . ModLoader . ApplyRomFsMods ( TitleId , dataStorage ) ;
2020-09-21 05:45:30 +02:00
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . SetRomFs ( newStorage . AsStream ( FileAccess . Read ) ) ;
2020-05-15 08:16:46 +02:00
}
2021-08-12 23:56:24 +02:00
// Don't create save data for system programs.
if ( TitleId ! = 0 & & ( TitleId < SystemProgramId . Start . Value | | TitleId > SystemAppletId . End . Value ) )
2020-05-15 08:16:46 +02:00
{
2021-08-12 23:56:24 +02:00
// Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
// We'll know if this changes in the future because stuff will get errors when trying to mount the correct save.
EnsureSaveData ( new ApplicationId ( TitleId & ~ 0xF ul ) ) ;
2020-05-15 08:16:46 +02:00
}
2020-07-09 06:31:15 +02:00
LoadExeFs ( codeFs , metaData ) ;
2020-08-04 01:32:53 +02:00
Logger . Info ? . Print ( LogClass . Loader , $"Application Loaded: {TitleName} v{DisplayVersion} [{TitleIdText}] [{(TitleIs64Bit ? " 64 - bit " : " 32 - bit ")}]" ) ;
2020-05-15 08:16:46 +02:00
}
2020-07-09 06:31:15 +02:00
// Sets TitleId, so be sure to call before using it
2021-08-12 23:56:24 +02:00
private MetaLoader ReadNpdm ( IFileSystem fs )
2020-07-09 06:31:15 +02:00
{
2021-12-23 17:55:50 +01:00
using var npdmFile = new UniqueRef < IFile > ( ) ;
Result result = fs . OpenFile ( ref npdmFile . Ref ( ) , "/main.npdm" . ToU8Span ( ) , OpenMode . Read ) ;
2020-09-21 05:45:30 +02:00
2021-08-12 23:56:24 +02:00
MetaLoader metaData ;
2020-07-09 06:31:15 +02:00
if ( ResultFs . PathNotFound . Includes ( result ) )
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Loader , "NPDM file not found, using default values!" ) ;
2020-07-09 06:31:15 +02:00
metaData = GetDefaultNpdm ( ) ;
}
else
{
2021-12-23 17:55:50 +01:00
npdmFile . Get . GetSize ( out long fileSize ) . ThrowIfFailure ( ) ;
2021-08-12 23:56:24 +02:00
var npdmBuffer = new byte [ fileSize ] ;
2021-12-23 17:55:50 +01:00
npdmFile . Get . Read ( out _ , 0 , npdmBuffer ) . ThrowIfFailure ( ) ;
2021-08-12 23:56:24 +02:00
metaData = new MetaLoader ( ) ;
metaData . Load ( npdmBuffer ) . ThrowIfFailure ( ) ;
2020-07-09 06:31:15 +02:00
}
2021-08-12 23:56:24 +02:00
metaData . GetNpdm ( out var npdm ) . ThrowIfFailure ( ) ;
TitleId = npdm . Aci . Value . ProgramId . Value ;
TitleIs64Bit = ( npdm . Meta . Value . Flags & 1 ) ! = 0 ;
_device . System . LibHacHorizonManager . ArpIReader . ApplicationId = new LibHac . ApplicationId ( TitleId ) ;
2020-07-09 06:31:15 +02:00
return metaData ;
}
2020-09-21 05:45:30 +02:00
private static void ReadControlData ( Switch device , Nca controlNca , ref BlitStruct < ApplicationControlProperty > controlData , ref string titleName , ref string displayVersion )
2020-05-15 08:16:46 +02:00
{
2021-12-23 17:55:50 +01:00
using var controlFile = new UniqueRef < IFile > ( ) ;
2020-09-21 05:45:30 +02:00
IFileSystem controlFs = controlNca . OpenFileSystem ( NcaSectionType . Data , device . System . FsIntegrityCheckLevel ) ;
2021-12-23 17:55:50 +01:00
Result result = controlFs . OpenFile ( ref controlFile . Ref ( ) , "/control.nacp" . ToU8Span ( ) , OpenMode . Read ) ;
2020-05-15 08:16:46 +02:00
if ( result . IsSuccess ( ) )
{
2021-12-23 17:55:50 +01:00
result = controlFile . Get . Read ( out long bytesRead , 0 , controlData . ByteSpan , ReadOption . None ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( result . IsSuccess ( ) & & bytesRead = = controlData . ByteSpan . Length )
2020-05-15 08:16:46 +02:00
{
2022-02-27 00:52:25 +01:00
titleName = controlData . Value . Title [ ( int ) device . System . State . DesiredTitleLanguage ] . NameString . ToString ( ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( string . IsNullOrWhiteSpace ( titleName ) )
2020-05-15 08:16:46 +02:00
{
2022-02-27 00:52:25 +01:00
titleName = controlData . Value . Title . ItemsRo . ToArray ( ) . FirstOrDefault ( x = > x . Name [ 0 ] ! = 0 ) . NameString . ToString ( ) ;
2020-05-15 08:16:46 +02:00
}
2022-02-27 00:52:25 +01:00
displayVersion = controlData . Value . DisplayVersionString . ToString ( ) ;
2020-05-15 08:16:46 +02:00
}
}
else
{
2020-09-21 05:45:30 +02:00
controlData . ByteSpan . Clear ( ) ;
2020-05-15 08:16:46 +02:00
}
}
2022-05-31 21:16:59 +02:00
private void LoadExeFs ( IFileSystem codeFs , MetaLoader metaData = null , bool isHomebrew = false )
2020-05-15 08:16:46 +02:00
{
2021-05-16 17:12:14 +02:00
if ( _device . Configuration . VirtualFileSystem . ModLoader . ReplaceExefsPartition ( TitleId , ref codeFs ) )
2020-05-15 08:16:46 +02:00
{
2022-05-05 20:23:30 +02:00
metaData = null ; // TODO: Check if we should retain old npdm.
2020-05-15 08:16:46 +02:00
}
2020-07-09 06:31:15 +02:00
metaData ? ? = ReadNpdm ( codeFs ) ;
2020-12-29 20:54:32 +01:00
NsoExecutable [ ] nsos = new NsoExecutable [ ExeFsPrefixes . Length ] ;
2020-05-15 08:16:46 +02:00
2021-01-03 12:30:31 +01:00
for ( int i = 0 ; i < nsos . Length ; i + + )
2020-05-15 08:16:46 +02:00
{
2020-12-29 20:54:32 +01:00
string name = ExeFsPrefixes [ i ] ;
2020-05-15 08:16:46 +02:00
2021-01-03 12:30:31 +01:00
if ( ! codeFs . FileExists ( $"/{name}" ) )
2020-12-29 20:54:32 +01:00
{
2022-05-05 20:23:30 +02:00
continue ; // File doesn't exist, skip.
2020-12-29 20:54:32 +01:00
}
2020-05-15 08:16:46 +02:00
2020-12-29 20:54:32 +01:00
Logger . Info ? . Print ( LogClass . Loader , $"Loading {name}..." ) ;
2020-05-15 08:16:46 +02:00
2021-12-23 17:55:50 +01:00
using var nsoFile = new UniqueRef < IFile > ( ) ;
codeFs . OpenFile ( ref nsoFile . Ref ( ) , $"/{name}" . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2020-05-15 08:16:46 +02:00
2021-12-23 17:55:50 +01:00
nsos [ i ] = new NsoExecutable ( nsoFile . Release ( ) . AsStorage ( ) , name ) ;
2020-05-15 08:16:46 +02:00
}
2022-05-05 20:23:30 +02:00
// ExeFs file replacements.
2021-05-16 17:12:14 +02:00
ModLoadResult modLoadResult = _device . Configuration . VirtualFileSystem . ModLoader . ApplyExefsMods ( TitleId , nsos ) ;
2020-12-29 20:54:32 +01:00
2022-05-05 20:23:30 +02:00
// Collect the nsos, ignoring ones that aren't used.
2020-12-29 20:54:32 +01:00
NsoExecutable [ ] programs = nsos . Where ( x = > x ! = null ) . ToArray ( ) ;
2020-07-09 06:31:15 +02:00
2022-05-05 20:23:30 +02:00
// Take the npdm from mods if present.
2020-12-29 20:54:32 +01:00
if ( modLoadResult . Npdm ! = null )
{
metaData = modLoadResult . Npdm ;
}
2020-05-15 08:16:46 +02:00
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . ModLoader . ApplyNsoPatches ( TitleId , programs ) ;
2020-05-15 08:16:46 +02:00
2021-05-16 17:12:14 +02:00
_device . Configuration . ContentManager . LoadEntries ( _device ) ;
2020-05-15 08:16:46 +02:00
2020-12-29 20:54:32 +01:00
bool usePtc = _device . System . EnablePtc ;
2021-05-13 20:05:15 +02:00
// Don't use PPTC if ExeFs files have been replaced.
2020-12-29 20:54:32 +01:00
usePtc & = ! modLoadResult . Modified ;
if ( _device . System . EnablePtc & & ! usePtc )
2020-07-09 06:31:15 +02:00
{
2021-05-13 20:05:15 +02:00
Logger . Warning ? . Print ( LogClass . Ptc , $"Detected unsupported ExeFs modifications. PPTC disabled." ) ;
2020-07-09 06:31:15 +02:00
}
2020-11-13 00:15:34 +01:00
Graphics . Gpu . GraphicsConfig . TitleId = TitleIdText ;
_device . Gpu . HostInitalized . Set ( ) ;
2022-05-03 01:30:02 +02:00
MemoryManagerMode memoryManagerMode = _device . Configuration . MemoryManagerMode ;
if ( ! MemoryBlock . SupportsFlags ( MemoryAllocationFlags . ViewCompatible ) )
{
memoryManagerMode = MemoryManagerMode . SoftwarePageTable ;
}
Ptc . Initialize ( TitleIdText , DisplayVersion , usePtc , memoryManagerMode ) ;
2020-06-16 20:28:02 +02:00
2022-05-03 23:28:32 +02:00
// We allow it for nx-hbloader because it can be used to launch homebrew.
2022-05-31 21:16:59 +02:00
bool allowCodeMemoryForJit = TitleId = = 0x010000000000100D UL | | isHomebrew ;
2022-05-03 23:28:32 +02:00
2021-08-12 23:56:24 +02:00
metaData . GetNpdm ( out Npdm npdm ) . ThrowIfFailure ( ) ;
2022-05-03 23:28:32 +02:00
ProgramInfo programInfo = new ProgramInfo ( in npdm , allowCodeMemoryForJit ) ;
ProgramLoader . LoadNsos ( _device . System . KernelContext , out ProcessTamperInfo tamperInfo , metaData , programInfo , executables : programs ) ;
2021-03-27 15:12:05 +01:00
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . ModLoader . LoadCheats ( TitleId , tamperInfo , _device . TamperMachine ) ;
2020-05-15 08:16:46 +02:00
}
public void LoadProgram ( string filePath )
{
2021-08-12 23:56:24 +02:00
MetaLoader metaData = GetDefaultNpdm ( ) ;
metaData . GetNpdm ( out Npdm npdm ) . ThrowIfFailure ( ) ;
2022-05-03 23:28:32 +02:00
ProgramInfo programInfo = new ProgramInfo ( in npdm , allowCodeMemoryForJit : true ) ;
2021-08-12 23:56:24 +02:00
bool isNro = Path . GetExtension ( filePath ) . ToLower ( ) = = ".nro" ;
2020-05-15 08:16:46 +02:00
2020-07-04 01:58:01 +02:00
IExecutable executable ;
2020-05-15 08:16:46 +02:00
if ( isNro )
{
2021-08-12 23:56:24 +02:00
FileStream input = new FileStream ( filePath , FileMode . Open ) ;
NroExecutable obj = new NroExecutable ( input . AsStorage ( ) ) ;
2020-09-21 05:45:30 +02:00
2020-07-04 01:58:01 +02:00
executable = obj ;
2020-05-15 08:16:46 +02:00
2022-05-05 20:23:30 +02:00
// Homebrew NRO can actually have some data after the actual NRO.
2020-05-15 08:16:46 +02:00
if ( input . Length > obj . FileSize )
{
input . Position = obj . FileSize ;
BinaryReader reader = new BinaryReader ( input ) ;
uint asetMagic = reader . ReadUInt32 ( ) ;
if ( asetMagic = = 0x54455341 )
{
uint asetVersion = reader . ReadUInt32 ( ) ;
if ( asetVersion = = 0 )
{
ulong iconOffset = reader . ReadUInt64 ( ) ;
2021-08-12 23:56:24 +02:00
ulong iconSize = reader . ReadUInt64 ( ) ;
2020-05-15 08:16:46 +02:00
ulong nacpOffset = reader . ReadUInt64 ( ) ;
2021-08-12 23:56:24 +02:00
ulong nacpSize = reader . ReadUInt64 ( ) ;
2020-05-15 08:16:46 +02:00
ulong romfsOffset = reader . ReadUInt64 ( ) ;
2021-08-12 23:56:24 +02:00
ulong romfsSize = reader . ReadUInt64 ( ) ;
2020-05-15 08:16:46 +02:00
if ( romfsSize ! = 0 )
{
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . SetRomFs ( new HomebrewRomFsStream ( input , obj . FileSize + ( long ) romfsOffset ) ) ;
2020-05-15 08:16:46 +02:00
}
if ( nacpSize ! = 0 )
{
input . Seek ( obj . FileSize + ( long ) nacpOffset , SeekOrigin . Begin ) ;
reader . Read ( ControlData . ByteSpan ) ;
ref ApplicationControlProperty nacp = ref ControlData . Value ;
2022-02-27 00:52:25 +01:00
programInfo . Name = nacp . Title [ ( int ) _device . System . State . DesiredTitleLanguage ] . NameString . ToString ( ) ;
2020-05-15 08:16:46 +02:00
2021-08-12 23:56:24 +02:00
if ( string . IsNullOrWhiteSpace ( programInfo . Name ) )
2020-05-15 08:16:46 +02:00
{
2022-02-27 00:52:25 +01:00
programInfo . Name = nacp . Title . ItemsRo . ToArray ( ) . FirstOrDefault ( x = > x . Name [ 0 ] ! = 0 ) . NameString . ToString ( ) ;
2020-05-15 08:16:46 +02:00
}
if ( nacp . PresenceGroupId ! = 0 )
{
2021-08-12 23:56:24 +02:00
programInfo . ProgramId = nacp . PresenceGroupId ;
2020-05-15 08:16:46 +02:00
}
2022-02-27 00:52:25 +01:00
else if ( nacp . SaveDataOwnerId ! = 0 )
2020-05-15 08:16:46 +02:00
{
2022-02-27 00:52:25 +01:00
programInfo . ProgramId = nacp . SaveDataOwnerId ;
2020-05-15 08:16:46 +02:00
}
else if ( nacp . AddOnContentBaseId ! = 0 )
{
2021-08-12 23:56:24 +02:00
programInfo . ProgramId = nacp . AddOnContentBaseId - 0x1000 ;
2020-05-15 08:16:46 +02:00
}
else
{
2021-08-12 23:56:24 +02:00
programInfo . ProgramId = 0000000000000000 ;
2020-05-15 08:16:46 +02:00
}
}
}
else
{
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Loader , $"Unsupported ASET header version found \" { asetVersion } \ "" ) ;
2020-05-15 08:16:46 +02:00
}
}
}
}
else
{
2020-07-09 06:31:15 +02:00
executable = new NsoExecutable ( new LocalStorage ( filePath , FileAccess . Read ) , Path . GetFileNameWithoutExtension ( filePath ) ) ;
2020-05-15 08:16:46 +02:00
}
2021-05-16 17:12:14 +02:00
_device . Configuration . ContentManager . LoadEntries ( _device ) ;
2020-05-15 08:16:46 +02:00
2021-08-12 23:56:24 +02:00
_titleName = programInfo . Name ;
TitleId = programInfo . ProgramId ;
TitleIs64Bit = ( npdm . Meta . Value . Flags & 1 ) ! = 0 ;
_device . System . LibHacHorizonManager . ArpIReader . ApplicationId = new LibHac . ApplicationId ( TitleId ) ;
2020-05-15 08:16:46 +02:00
2022-05-05 20:23:30 +02:00
// Explicitly null titleid to disable the shader cache.
2020-11-13 00:15:34 +01:00
Graphics . Gpu . GraphicsConfig . TitleId = null ;
_device . Gpu . HostInitalized . Set ( ) ;
2021-08-12 23:56:24 +02:00
ProgramLoader . LoadNsos ( _device . System . KernelContext , out ProcessTamperInfo tamperInfo , metaData , programInfo , executables : executable ) ;
2021-03-27 15:12:05 +01:00
2021-05-16 17:12:14 +02:00
_device . Configuration . VirtualFileSystem . ModLoader . LoadCheats ( TitleId , tamperInfo , _device . TamperMachine ) ;
2020-05-15 08:16:46 +02:00
}
2021-08-12 23:56:24 +02:00
private MetaLoader GetDefaultNpdm ( )
2020-05-15 08:16:46 +02:00
{
Assembly asm = Assembly . GetCallingAssembly ( ) ;
using ( Stream npdmStream = asm . GetManifestResourceStream ( "Ryujinx.HLE.Homebrew.npdm" ) )
{
2021-08-12 23:56:24 +02:00
var npdmBuffer = new byte [ npdmStream . Length ] ;
npdmStream . Read ( npdmBuffer ) ;
var metaLoader = new MetaLoader ( ) ;
metaLoader . Load ( npdmBuffer ) . ThrowIfFailure ( ) ;
return metaLoader ;
}
}
private static ( ulong applicationId , int programCount ) GetMultiProgramInfo ( VirtualFileSystem fileSystem , PartitionFileSystem pfs )
{
ulong mainProgramId = 0 ;
Span < bool > hasIndex = stackalloc bool [ 0x10 ] ;
fileSystem . ImportTickets ( pfs ) ;
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*.nca" ) )
{
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 ( ) ;
2021-08-12 23:56:24 +02:00
2021-12-23 17:55:50 +01:00
Nca nca = new Nca ( fileSystem . KeySet , ncaFile . Release ( ) . AsStorage ( ) ) ;
2021-08-12 23:56:24 +02:00
if ( nca . Header . ContentType ! = NcaContentType . Program )
{
continue ;
}
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
2021-12-23 17:55:50 +01:00
if ( nca . SectionExists ( NcaSectionType . Data ) & & nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
2021-08-12 23:56:24 +02:00
{
continue ;
}
ulong currentProgramId = nca . Header . TitleId ;
ulong currentMainProgramId = currentProgramId & ~ 0xFFF ul ;
if ( mainProgramId = = 0 & & currentMainProgramId ! = 0 )
{
mainProgramId = currentMainProgramId ;
}
if ( mainProgramId ! = currentMainProgramId )
{
// As far as I know there aren't any multi-application game cards containing multi-program applications,
// so because multi-application game cards are the only way we should run into multiple applications
// we'll just return that there's a single program.
return ( mainProgramId , 1 ) ;
}
hasIndex [ ( int ) ( currentProgramId & 0xF ) ] = true ;
}
int programCount = 0 ;
for ( int i = 0 ; i < hasIndex . Length & & hasIndex [ i ] ; i + + )
{
programCount + + ;
2020-05-15 08:16:46 +02:00
}
2021-08-12 23:56:24 +02:00
return ( mainProgramId , programCount ) ;
}
private Result RegisterProgramMapInfo ( PartitionFileSystem pfs )
{
( ulong applicationId , int programCount ) = GetMultiProgramInfo ( _device . Configuration . VirtualFileSystem , pfs ) ;
if ( programCount < = 0 )
return Result . Success ;
Span < ProgramIndexMapInfo > mapInfo = stackalloc ProgramIndexMapInfo [ 0x10 ] ;
for ( int i = 0 ; i < programCount ; i + + )
{
mapInfo [ i ] . ProgramId = new ProgramId ( applicationId + ( uint ) i ) ;
mapInfo [ i ] . MainProgramId = new ProgramId ( applicationId ) ;
mapInfo [ i ] . ProgramIndex = ( byte ) i ;
}
return _device . System . LibHacHorizonManager . NsClient . Fs . RegisterProgramIndexMapInfo ( mapInfo . Slice ( 0 , programCount ) ) ;
2020-05-15 08:16:46 +02:00
}
2020-09-01 22:08:59 +02:00
private Result EnsureSaveData ( ApplicationId applicationId )
2020-05-15 08:16:46 +02:00
{
2020-08-04 01:32:53 +02:00
Logger . Info ? . Print ( LogClass . Application , "Ensuring required savedata exists." ) ;
2020-05-15 08:16:46 +02:00
2021-04-13 03:16:43 +02:00
Uid user = _device . System . AccountManager . LastOpenedUser . UserId . ToLibHacUid ( ) ;
2020-05-15 08:16:46 +02:00
ref ApplicationControlProperty control = ref ControlData . Value ;
2022-01-12 12:22:19 +01:00
if ( LibHac . Common . Utilities . IsZeros ( ControlData . ByteSpan ) )
2020-05-15 08:16:46 +02:00
{
// If the current application doesn't have a loaded control property, create a dummy one
// and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct < ApplicationControlProperty > ( 1 ) . Value ;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control . UserAccountSaveDataSize = 0x4000 ;
control . UserAccountSaveDataJournalSize = 0x4000 ;
2022-02-27 00:52:25 +01:00
control . SaveDataOwnerId = applicationId . Value ;
2020-05-15 08:16:46 +02:00
2020-08-04 01:32:53 +02:00
Logger . Warning ? . Print ( LogClass . Application ,
2020-05-15 08:16:46 +02:00
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games." ) ;
}
2021-08-12 23:56:24 +02:00
HorizonClient hos = _device . System . LibHacHorizonManager . RyujinxClient ;
2022-02-27 00:52:25 +01:00
Result resultCode = hos . Fs . EnsureApplicationCacheStorage ( out _ , out _ , applicationId , in control ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( resultCode . IsFailure ( ) )
2020-05-15 08:16:46 +02:00
{
2020-09-21 05:45:30 +02:00
Logger . Error ? . Print ( LogClass . Application , $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}" ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
return resultCode ;
2020-05-15 08:16:46 +02:00
}
2022-02-27 00:52:25 +01:00
resultCode = hos . Fs . EnsureApplicationSaveData ( out _ , applicationId , in control , in user ) ;
2020-05-15 08:16:46 +02:00
2020-09-21 05:45:30 +02:00
if ( resultCode . IsFailure ( ) )
2020-05-15 08:16:46 +02:00
{
2020-09-21 05:45:30 +02:00
Logger . Error ? . Print ( LogClass . Application , $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}" ) ;
2020-05-15 08:16:46 +02:00
}
2020-09-21 05:45:30 +02:00
return resultCode ;
2020-05-15 08:16:46 +02:00
}
}
2020-12-29 20:54:32 +01:00
}