2022-05-15 13:30:15 +02:00
using Avalonia.Controls ;
2023-01-21 02:57:37 +01:00
using Avalonia.Controls.Notifications ;
2022-05-15 13:30:15 +02:00
using Avalonia.Threading ;
using LibHac ;
using LibHac.Account ;
using LibHac.Common ;
using LibHac.Fs ;
using LibHac.Fs.Fsa ;
using LibHac.Fs.Shim ;
using LibHac.FsSystem ;
using LibHac.Ns ;
using LibHac.Tools.Fs ;
using LibHac.Tools.FsSystem ;
using LibHac.Tools.FsSystem.NcaUtils ;
using Ryujinx.Ava.Common.Locale ;
2022-12-29 15:24:05 +01:00
using Ryujinx.Ava.UI.Helpers ;
using Ryujinx.Ava.UI.Windows ;
2022-05-15 13:30:15 +02:00
using Ryujinx.Common.Logging ;
using Ryujinx.HLE.FileSystem ;
using Ryujinx.HLE.HOS ;
using Ryujinx.HLE.HOS.Services.Account.Acc ;
using Ryujinx.Ui.Common.Helper ;
using System ;
using System.Buffers ;
using System.IO ;
using System.Threading ;
2022-07-12 00:25:33 +02:00
using System.Threading.Tasks ;
2022-05-15 13:30:15 +02:00
using Path = System . IO . Path ;
namespace Ryujinx.Ava.Common
{
2022-07-05 20:06:31 +02:00
internal static class ApplicationHelper
2022-05-15 13:30:15 +02:00
{
private static HorizonClient _horizonClient ;
private static AccountManager _accountManager ;
private static VirtualFileSystem _virtualFileSystem ;
private static StyleableWindow _owner ;
public static void Initialize ( VirtualFileSystem virtualFileSystem , AccountManager accountManager , HorizonClient horizonClient , StyleableWindow owner )
{
_owner = owner ;
_virtualFileSystem = virtualFileSystem ;
_horizonClient = horizonClient ;
_accountManager = accountManager ;
}
2023-01-21 02:57:37 +01:00
private static bool TryFindSaveData ( string titleName , ulong titleId , BlitStruct < ApplicationControlProperty > controlHolder , in SaveDataFilter filter , out ulong saveDataId )
2022-05-15 13:30:15 +02:00
{
saveDataId = default ;
2023-01-21 02:57:37 +01:00
Result result = _horizonClient . Fs . FindSaveDataWithFilter ( out SaveDataInfo saveDataInfo , SaveDataSpaceId . User , in filter ) ;
2022-05-15 13:30:15 +02:00
if ( ResultFs . TargetNotFound . Includes ( result ) )
{
ref ApplicationControlProperty control = ref controlHolder . Value ;
Logger . Info ? . Print ( LogClass . Application , $"Creating save directory for Title: {titleName} [{titleId:x16}]" ) ;
if ( Utilities . IsZeros ( controlHolder . ByteSpan ) )
{
// 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 ;
2023-01-21 02:57:37 +01:00
Logger . Warning ? . Print ( LogClass . Application , "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games." ) ;
2022-05-15 13:30:15 +02:00
}
2023-01-21 02:57:37 +01:00
Uid user = new ( ( ulong ) _accountManager . LastOpenedUser . UserId . High , ( ulong ) _accountManager . LastOpenedUser . UserId . Low ) ;
2022-05-15 13:30:15 +02:00
result = _horizonClient . Fs . EnsureApplicationSaveData ( out _ , new LibHac . Ncm . ApplicationId ( titleId ) , in control , in user ) ;
if ( result . IsFailure ( ) )
{
2023-01-21 02:57:37 +01:00
Dispatcher . UIThread . InvokeAsync ( async ( ) = >
2022-07-12 00:25:33 +02:00
{
2023-01-21 02:06:19 +01:00
await ContentDialogHelper . CreateErrorDialog ( LocaleManager . Instance . UpdateAndGetDynamicValue ( LocaleKeys . DialogMessageCreateSaveErrorMessage , result . ToStringWithName ( ) ) ) ;
2022-07-12 00:25:33 +02:00
} ) ;
2022-05-15 13:30:15 +02:00
return false ;
}
// Try to find the savedata again after creating it
result = _horizonClient . Fs . FindSaveDataWithFilter ( out saveDataInfo , SaveDataSpaceId . User , in filter ) ;
}
if ( result . IsSuccess ( ) )
{
saveDataId = saveDataInfo . SaveDataId ;
return true ;
}
2023-01-21 02:57:37 +01:00
Dispatcher . UIThread . InvokeAsync ( async ( ) = >
2022-07-12 00:25:33 +02:00
{
2023-01-21 02:06:19 +01:00
await ContentDialogHelper . CreateErrorDialog ( LocaleManager . Instance . UpdateAndGetDynamicValue ( LocaleKeys . DialogMessageFindSaveErrorMessage , result . ToStringWithName ( ) ) ) ;
2022-07-12 00:25:33 +02:00
} ) ;
2022-05-15 13:30:15 +02:00
return false ;
}
2023-01-21 02:57:37 +01:00
public static void OpenSaveDir ( in SaveDataFilter saveDataFilter , ulong titleId , BlitStruct < ApplicationControlProperty > controlData , string titleName )
2022-05-15 13:30:15 +02:00
{
if ( ! TryFindSaveData ( titleName , titleId , controlData , in saveDataFilter , out ulong saveDataId ) )
{
return ;
}
2022-12-02 14:16:43 +01:00
OpenSaveDir ( saveDataId ) ;
}
public static void OpenSaveDir ( ulong saveDataId )
{
2022-05-15 13:30:15 +02:00
string saveRootPath = Path . Combine ( _virtualFileSystem . GetNandPath ( ) , $"user/save/{saveDataId:x16}" ) ;
if ( ! Directory . Exists ( saveRootPath ) )
{
// Inconsistent state. Create the directory
Directory . CreateDirectory ( saveRootPath ) ;
}
string committedPath = Path . Combine ( saveRootPath , "0" ) ;
string workingPath = Path . Combine ( saveRootPath , "1" ) ;
// If the committed directory exists, that path will be loaded the next time the savedata is mounted
if ( Directory . Exists ( committedPath ) )
{
OpenHelper . OpenFolder ( committedPath ) ;
}
else
{
// If the working directory exists and the committed directory doesn't,
// the working directory will be loaded the next time the savedata is mounted
if ( ! Directory . Exists ( workingPath ) )
{
Directory . CreateDirectory ( workingPath ) ;
}
OpenHelper . OpenFolder ( workingPath ) ;
}
}
2023-01-21 02:57:37 +01:00
public static async Task ExtractSection ( NcaSectionType ncaSectionType , string titleFilePath , string titleName , int programIndex = 0 )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
OpenFolderDialog folderDialog = new ( )
{
Title = LocaleManager . Instance [ LocaleKeys . FolderDialogExtractTitle ]
} ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
string destination = await folderDialog . ShowAsync ( _owner ) ;
var cancellationToken = new CancellationTokenSource ( ) ;
2022-05-15 13:30:15 +02:00
if ( ! string . IsNullOrWhiteSpace ( destination ) )
{
Thread extractorThread = new ( ( ) = >
{
Dispatcher . UIThread . Post ( async ( ) = >
{
UserResult result = await ContentDialogHelper . CreateConfirmationDialog (
2023-01-21 02:06:19 +01:00
LocaleManager . Instance . UpdateAndGetDynamicValue ( LocaleKeys . DialogNcaExtractionMessage , ncaSectionType , Path . GetFileName ( titleFilePath ) ) ,
2022-05-15 13:30:15 +02:00
"" ,
"" ,
2023-01-03 19:45:08 +01:00
LocaleManager . Instance [ LocaleKeys . InputDialogCancel ] ,
LocaleManager . Instance [ LocaleKeys . DialogNcaExtractionTitle ] ) ;
2022-05-15 13:30:15 +02:00
if ( result = = UserResult . Cancel )
{
cancellationToken . Cancel ( ) ;
}
} ) ;
2023-01-21 02:57:37 +01:00
using FileStream file = new ( titleFilePath , FileMode . Open , FileAccess . Read ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
Nca mainNca = null ;
Nca patchNca = null ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
string extension = Path . GetExtension ( titleFilePath ) . ToLower ( ) ;
if ( extension = = ".nsp" | | extension = = ".pfs0" | | extension = = ".xci" )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
PartitionFileSystem pfs ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
if ( extension = = ".xci" )
{
pfs = new Xci ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) . OpenPartition ( XciPartitionType . Secure ) ;
}
else
{
pfs = new PartitionFileSystem ( file . AsStorage ( ) ) ;
}
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*.nca" ) )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
using var ncaFile = new UniqueRef < IFile > ( ) ;
2022-05-15 13:30:15 +02:00
2023-03-02 03:42:27 +01:00
pfs . OpenFile ( ref ncaFile . Ref , fileEntry . FullPath . ToU8Span ( ) , OpenMode . Read ) . ThrowIfFailure ( ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
Nca nca = new ( _virtualFileSystem . KeySet , ncaFile . Get . AsStorage ( ) ) ;
if ( nca . Header . ContentType = = NcaContentType . Program )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
if ( nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
{
patchNca = nca ;
}
else
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
mainNca = nca ;
2022-05-15 13:30:15 +02:00
}
}
}
2023-01-21 02:57:37 +01:00
}
else if ( extension = = ".nca" )
{
mainNca = new Nca ( _virtualFileSystem . KeySet , file . AsStorage ( ) ) ;
}
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
if ( mainNca = = null )
{
Logger . Error ? . Print ( LogClass . Application , "Extraction failure. The main NCA was not present in the selected file" ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
Dispatcher . UIThread . InvokeAsync ( async ( ) = >
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
await ContentDialogHelper . CreateErrorDialog ( LocaleManager . Instance [ LocaleKeys . DialogNcaExtractionMainNcaNotFoundErrorMessage ] ) ;
} ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
return ;
}
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
( Nca updatePatchNca , _ ) = ApplicationLoader . GetGameUpdateData ( _virtualFileSystem , mainNca . Header . TitleId . ToString ( "x16" ) , programIndex , out _ ) ;
if ( updatePatchNca ! = null )
{
patchNca = updatePatchNca ;
}
int index = Nca . GetSectionIndexFromType ( ncaSectionType , mainNca . Header . ContentType ) ;
try
{
IFileSystem ncaFileSystem = patchNca ! = null
? mainNca . OpenFileSystemWithPatch ( patchNca , index , IntegrityCheckLevel . ErrorOnInvalid )
: mainNca . OpenFileSystem ( index , IntegrityCheckLevel . ErrorOnInvalid ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
FileSystemClient fsClient = _horizonClient . Fs ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
string source = DateTime . Now . ToFileTime ( ) . ToString ( ) [ 10. . ] ;
string output = DateTime . Now . ToFileTime ( ) . ToString ( ) [ 10. . ] ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
using var uniqueSourceFs = new UniqueRef < IFileSystem > ( ncaFileSystem ) ;
using var uniqueOutputFs = new UniqueRef < IFileSystem > ( new LocalFileSystem ( destination ) ) ;
2022-05-15 13:30:15 +02:00
2023-03-02 03:42:27 +01:00
fsClient . Register ( source . ToU8Span ( ) , ref uniqueSourceFs . Ref ) ;
fsClient . Register ( output . ToU8Span ( ) , ref uniqueOutputFs . Ref ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
( Result ? resultCode , bool canceled ) = CopyDirectory ( fsClient , $"{source}:/" , $"{output}:/" , cancellationToken . Token ) ;
2022-05-15 13:30:15 +02:00
2023-01-21 02:57:37 +01:00
if ( ! canceled )
{
if ( resultCode . Value . IsFailure ( ) )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
Logger . Error ? . Print ( LogClass . Application , $"LibHac returned error code: {resultCode.Value.ErrorCode}" ) ;
Dispatcher . UIThread . InvokeAsync ( async ( ) = >
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
await ContentDialogHelper . CreateErrorDialog ( LocaleManager . Instance [ LocaleKeys . DialogNcaExtractionCheckLogErrorMessage ] ) ;
} ) ;
2022-05-15 13:30:15 +02:00
}
2023-01-21 02:57:37 +01:00
else if ( resultCode . Value . IsSuccess ( ) )
2022-05-15 13:30:15 +02:00
{
2023-01-21 02:57:37 +01:00
NotificationHelper . Show (
LocaleManager . Instance [ LocaleKeys . DialogNcaExtractionTitle ] ,
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}" ,
NotificationType . Information ) ;
}
2022-05-15 13:30:15 +02:00
}
2023-01-21 02:57:37 +01:00
fsClient . Unmount ( source . ToU8Span ( ) ) ;
fsClient . Unmount ( output . ToU8Span ( ) ) ;
}
catch ( ArgumentException ex )
{
Logger . Error ? . Print ( LogClass . Application , $"{ex.Message}" ) ;
Dispatcher . UIThread . InvokeAsync ( async ( ) = >
{
await ContentDialogHelper . CreateErrorDialog ( ex . Message ) ;
} ) ;
2022-05-15 13:30:15 +02:00
}
} ) ;
extractorThread . Name = "GUI.NcaSectionExtractorThread" ;
extractorThread . IsBackground = true ;
extractorThread . Start ( ) ;
}
}
public static ( Result ? result , bool canceled ) CopyDirectory ( FileSystemClient fs , string sourcePath , string destPath , CancellationToken token )
{
Result rc = fs . OpenDirectory ( out DirectoryHandle sourceHandle , sourcePath . ToU8Span ( ) , OpenDirectoryMode . All ) ;
if ( rc . IsFailure ( ) )
{
return ( rc , false ) ;
}
using ( sourceHandle )
{
foreach ( DirectoryEntryEx entry in fs . EnumerateEntries ( sourcePath , "*" , SearchOptions . Default ) )
{
if ( token . IsCancellationRequested )
{
return ( null , true ) ;
}
string subSrcPath = PathTools . Normalize ( PathTools . Combine ( sourcePath , entry . Name ) ) ;
string subDstPath = PathTools . Normalize ( PathTools . Combine ( destPath , entry . Name ) ) ;
if ( entry . Type = = DirectoryEntryType . Directory )
{
fs . EnsureDirectoryExists ( subDstPath ) ;
( Result ? result , bool canceled ) = CopyDirectory ( fs , subSrcPath , subDstPath , token ) ;
if ( canceled | | result . Value . IsFailure ( ) )
{
return ( result , canceled ) ;
}
}
if ( entry . Type = = DirectoryEntryType . File )
{
fs . CreateOrOverwriteFile ( subDstPath , entry . Size ) ;
rc = CopyFile ( fs , subSrcPath , subDstPath ) ;
if ( rc . IsFailure ( ) )
{
return ( rc , false ) ;
}
}
}
}
return ( Result . Success , false ) ;
}
public static Result CopyFile ( FileSystemClient fs , string sourcePath , string destPath )
{
Result rc = fs . OpenFile ( out FileHandle sourceHandle , sourcePath . ToU8Span ( ) , OpenMode . Read ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
using ( sourceHandle )
{
rc = fs . OpenFile ( out FileHandle destHandle , destPath . ToU8Span ( ) , OpenMode . Write | OpenMode . AllowAppend ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
using ( destHandle )
{
const int MaxBufferSize = 1024 * 1024 ;
rc = fs . GetFileSize ( out long fileSize , sourceHandle ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
int bufferSize = ( int ) Math . Min ( MaxBufferSize , fileSize ) ;
byte [ ] buffer = ArrayPool < byte > . Shared . Rent ( bufferSize ) ;
try
{
for ( long offset = 0 ; offset < fileSize ; offset + = bufferSize )
{
int toRead = ( int ) Math . Min ( fileSize - offset , bufferSize ) ;
Span < byte > buf = buffer . AsSpan ( 0 , toRead ) ;
rc = fs . ReadFile ( out long _ , sourceHandle , offset , buf ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
rc = fs . WriteFile ( destHandle , offset , buf , WriteOption . None ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
}
}
finally
{
ArrayPool < byte > . Shared . Return ( buffer ) ;
}
rc = fs . FlushFile ( destHandle ) ;
if ( rc . IsFailure ( ) )
{
return rc ;
}
}
}
return Result . Success ;
}
}
}