diff --git a/Ryujinx.Ava/Common/SaveDataFileManager.cs b/Ryujinx.Ava/Common/SaveDataFileManager.cs new file mode 100644 index 000000000..8a73529ec --- /dev/null +++ b/Ryujinx.Ava/Common/SaveDataFileManager.cs @@ -0,0 +1,357 @@ +using LibHac; +using LibHac.Account; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Fs.Shim; +using LibHac.Ns; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Models; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using Logger = Ryujinx.Common.Logging.Logger; +using Path = System.IO.Path; +using Result = LibHac.Result; +using UserId = LibHac.Fs.UserId; +using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile; + +namespace Ryujinx.Ava.Common +{ + internal class SaveDataFileManager + { + private readonly UserProfile _userProfile; + private readonly HorizonClient _horizonClient; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly UserId _userId; + + private readonly List _applications; + + public readonly string mountName = "save"; + public readonly string outputMountName = "output"; + + public SaveDataFileManager(List applications, UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem, UserId userId) + { + _userProfile = userProfile; + _horizonClient = horizonClient; + _virtualFileSystem = virtualFileSystem; + _applications = applications; + _userId = userId; + } + + + #region Export + + public void SaveUserSaveDirectoryAsZip(string backupFolder, List saves) + { + CreateSaveDataBackup(backupFolder, saves); + + ZipFile.CreateFromDirectory(backupFolder, backupFolder + ".zip"); + Directory.Delete(backupFolder, true); + } + + private void CreateSaveDataBackup(string backupPath, List saves) + { + U8Span mountNameU8 = mountName.ToU8Span(); + U8Span outputMountNameU8 = outputMountName.ToU8Span(); + + foreach (ApplicationData application in _applications) + { + try + { + //Register destination folder as output and mount output + Result registerOutpDirResult = RegisterOutputDirectory(Path.Combine(backupPath, application.TitleId.ToUpper()), outputMountNameU8); + if (registerOutpDirResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not register and mount output directory."); + } + + //Mount SaveData as save, opens the saveDataIterators and starts reading saveDataInfo + Result openAndReadSaveDataResult = OpenSaveDataIteratorAndReadSaveData(mountNameU8, application, out SaveDataInfo saveDataInfo); + + if (openAndReadSaveDataResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not open save Iterator and start reading for application: {application.TitleName}"); + } + else + { + //Copies the whole directory from save mount to output mount + Result copyDirResult = CopySaveDataDirectory(mountName, outputMountName); + Logger.Info.Value.Print(LogClass.Application, $"Successfuly created backup for {application.TitleName}."); + } + + //Unmount save and output + UnmountDirectory(mountNameU8); + UnmountDirectory(outputMountNameU8); + } + catch (Exception) + { + continue; + } + } + } + + + #endregion + + + + #region Import + + public void RestoreSavedataBackup(string backupZipFile) + { + ExtractBackupToSaveDirectory(backupZipFile); + + Logger.Info.Value.Print(LogClass.Application, $"Done extracting savedata backup!", nameof(SaveDataImporter)); + } + + private void ExtractBackupToSaveDirectory(string backupZipFile) + { + if (!string.IsNullOrWhiteSpace(backupZipFile) && File.Exists(backupZipFile)) + { + string extractedZipFolders = ExtractZip(backupZipFile); + + Logger.Info.Value.Print(LogClass.Application, $"Extracted Backup zip to temp path: {extractedZipFolders}", nameof(SaveDataImporter)); + + U8Span mountNameU8 = mountName.ToU8Span(); + U8Span outputMountNameU8 = outputMountName.ToU8Span(); + + + foreach (ApplicationData application in _applications) + { + try + { + string backupTitleSaveDataFolder = Path.Combine(Directory.GetParent(extractedZipFolders).FullName, application.TitleId.ToUpper()); + + //Register destination folder as output and mount output + Result registerSaveDataBackupFolderResult = RegisterOutputDirectory(backupTitleSaveDataFolder, outputMountNameU8); + if (registerSaveDataBackupFolderResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not register and mount output directory."); + } + + //Mount SaveData as save, opens the saveDataIterators and starts reading saveDataInfo + Result openAndReadSaveDataResult = OpenSaveDataIteratorAndReadSaveData(mountNameU8, application, out SaveDataInfo saveDataInfo); + + if (openAndReadSaveDataResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not open save Iterator and start reading for application: {application.TitleName}"); + } + else + { + //Copies the whole directory from backup mount to saveData mount + Result copyDirResult = CopySaveDataDirectory(outputMountName, mountName); + Logger.Info.Value.Print(LogClass.Application, $"Successfuly restored backup for: {application.TitleName}."); + } + + Result commitOuptut = _horizonClient.Fs.Commit(outputMountNameU8); + + //Unmount save and output + UnmountDirectory(mountNameU8); + UnmountDirectory(outputMountNameU8); + } + catch (Exception) + { + continue; + } + } + } + } + + private string ExtractZip(string backupZipFile) + { + string tempZipExtractionPath = Path.GetTempPath(); + ZipFile.ExtractToDirectory(backupZipFile, tempZipExtractionPath, true); + + return tempZipExtractionPath; + } + + + #endregion + + + #region Horizon OS Open/Register/Read/Mount Stuff + + private Result RegisterOutputDirectory(string backupPath, U8Span mountName) + { + using UniqueRef outputFileSystem = new UniqueRef(new LibHac.FsSystem.LocalFileSystem(backupPath)); + + Result registerResult = _horizonClient.Fs.Register(mountName, ref outputFileSystem.Ref()); + if (registerResult.IsFailure()) return registerResult.Miss(); + + return registerResult; + } + + private Result GetSaveDataIterator(out SaveDataInfo saveDataInfo, ApplicationData application) + { + return _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, + SaveDataSpaceId.User, + SaveDataFilter.Make(ulong.Parse(application.TitleId, NumberStyles.HexNumber), + saveType: default, + _userId, + saveDataId: default, + index: default)); + } + + private Result MountSaveDataDirectory(ulong programId, U8Span mountName) + { + if (!_horizonClient.Fs.IsMounted(mountName)) + { + return _horizonClient.Fs.MountSaveData(mountName, ConvertProgramIdToApplicationId(programId), _userId); + } + + return Result.Success; + } + + private Result UnmountDirectory(U8Span mountName) + { + if (_horizonClient.Fs.IsMounted(mountName)) + { + Result commitFilesResult = _horizonClient.Fs.CommitSaveData(mountName); + if (commitFilesResult.IsFailure()) return commitFilesResult; + + _horizonClient.Fs.Unmount(mountName); + } + + return Result.Success; + } + + private bool CreateSaveData(ApplicationData app) + { + ref ApplicationControlProperty control = ref app.ControlHolder.Value; + + Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {app.TitleName} [{app.TitleId:x16}]"); + + if (Utilities.IsZeros(app.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(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; + + 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."); + } + + Uid user = new Uid(ulong.Parse(_userProfile.UserId.High.ToString(), NumberStyles.HexNumber), + ulong.Parse(_userProfile.UserId.Low.ToString(), NumberStyles.HexNumber)); + + Result findSaveDataResult = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(ulong.Parse(app.TitleId, NumberStyles.HexNumber)), in control, in user); + + return findSaveDataResult.IsSuccess(); + } + + private Result OpenSaveDataIteratorAndReadSaveData(U8Span mountName, ApplicationData application, out SaveDataInfo saveDataInfo) + { + Result getSvDataIteratorResult = GetSaveDataIterator(out saveDataInfo, application); + if (getSvDataIteratorResult.IsFailure()) + { + bool createdSaveData = CreateSaveData(application); + + if (!createdSaveData) + { + Logger.Warning?.Print(LogClass.Application, "Could not create saveData for " + application.TitleName); + return getSvDataIteratorResult; + } + + getSvDataIteratorResult = GetSaveDataIterator(out saveDataInfo, application); + if (getSvDataIteratorResult.IsFailure()) return getSvDataIteratorResult; + } + + Result mountSvDataResult = MountSaveDataDirectory(saveDataInfo.ProgramId.Value, mountName); + if (mountSvDataResult.IsFailure()) return mountSvDataResult; + + UniqueRef saveDataIterator = new UniqueRef(); + Result openSvDataIteratorResult = _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator, SaveDataSpaceId.User); + if (openSvDataIteratorResult.IsFailure()) return openSvDataIteratorResult; + + Result readSvDataInfoResult = saveDataIterator.Get.ReadSaveDataInfo(out _, new Span(ref saveDataInfo)); + if (openSvDataIteratorResult.IsFailure()) return readSvDataInfoResult; + + return Result.Success; + } + + + #endregion + + + #region Copy Directory/File + private Result CopySaveDataDirectory(string sourcePath, string destPath) + { + Result openDir = _horizonClient.Fs.OpenDirectory(out DirectoryHandle dirHandle, $"{sourcePath}:/".ToU8Span(), OpenDirectoryMode.All); + if (openDir.IsFailure()) return openDir; + + using (dirHandle) + { + sourcePath = $"{sourcePath}:/"; + destPath = $"{destPath}:/"; + foreach (DirectoryEntryEx entry in _horizonClient.Fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) + { + string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); + string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); + + if (entry.Type == DirectoryEntryType.Directory) + { + Result copyDirResult = CopyDirectory(subSrcPath, subDstPath); + } + if (entry.Type == DirectoryEntryType.File) + { + Result copyFinalDir = CopyFile(subSrcPath, subDstPath, entry.Size); + } + } + + _horizonClient.Fs.CloseDirectory(dirHandle); + + return Result.Success; + } + } + + private Result CopyDirectory(string subSrcPath, string subDstPath) + { + _horizonClient.Fs.EnsureDirectoryExists(subDstPath); + + Result copyDirResult = _horizonClient.Fs.CopyDirectory(subSrcPath, subDstPath); + + if (copyDirResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not copy directory: \n{subSrcPath} to destination {subDstPath}"); + } + + Logger.Info.Value.Print(LogClass.Application, $"Successfully copied directory: \n{subSrcPath} to destination {subDstPath}"); + + return copyDirResult; + } + + private Result CopyFile(string subSrcPath, string subDstPath, long entrySize) + { + _horizonClient.Fs.CreateOrOverwriteFile(subDstPath, entrySize); + + Result copyFileResult = _horizonClient.Fs.CopyFile(subSrcPath, subDstPath); + if (copyFileResult.IsFailure()) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not copy file: \n{subSrcPath} to destination '{subDstPath}"); + } + + Logger.Info.Value.Print(LogClass.Application, $"Successfully copied file: \n{subSrcPath} to destination {subDstPath}"); + + return copyFileResult; + } + + #endregion + + private LibHac.Ncm.ApplicationId ConvertProgramIdToApplicationId(ulong programId) + { + return new LibHac.Ncm.ApplicationId(programId); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/SaveDataExporter.cs b/Ryujinx.Ava/Ui/Controls/SaveDataExporter.cs index 148a3d52c..efdae1288 100644 --- a/Ryujinx.Ava/Ui/Controls/SaveDataExporter.cs +++ b/Ryujinx.Ava/Ui/Controls/SaveDataExporter.cs @@ -1,224 +1,36 @@ using Avalonia.Controls; using LibHac; -using LibHac.Common; using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Fs.Shim; -using LibHac.Tools.Fs; -using LibHac.Tools.FsSystem; +using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.Windows; -using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.App.Common; -using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.IO.Compression; using System.Threading.Tasks; -using Logger = Ryujinx.Common.Logging.Logger; -using Path = System.IO.Path; +using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile; namespace Ryujinx.Ava.Ui.Controls { internal class SaveDataExporter { - private readonly UserProfile _userProfile; - private readonly HorizonClient _horizonClient; - private readonly VirtualFileSystem _virtualFileSystem; - private readonly UserId _userId; + private readonly SaveDataFileManager saveDataFileManager; - public SaveDataExporter(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem) + public SaveDataExporter(List applications, UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem) { - _userProfile = userProfile; - _horizonClient = horizonClient; - _virtualFileSystem = virtualFileSystem; - _userId = new UserId( - ulong.Parse(_userProfile.UserId.High.ToString(), System.Globalization.NumberStyles.HexNumber), - ulong.Parse(_userProfile.UserId.Low.ToString(), System.Globalization.NumberStyles.HexNumber) - ); + UserId userId = new UserId(ulong.Parse(userProfile.UserId.High.ToString(), NumberStyles.HexNumber), + ulong.Parse(userProfile.UserId.Low.ToString(), NumberStyles.HexNumber)); + + saveDataFileManager = new SaveDataFileManager(applications, userProfile, horizonClient, virtualFileSystem, userId); } - public async void SaveUserSaveDirectoryAsZip(MainWindow mainWindow, List saves, List applications) + public async void SaveUserSaveDirectoryAsZip(MainWindow mainWindow, List saves) { string backupFolder = await GetAndPrepareBackupPath(mainWindow); - CreateBackup(backupFolder, saves, applications); - - ZipFile.CreateFromDirectory(backupFolder, backupFolder + ".zip"); - Directory.Delete(backupFolder, true); - } - - private void CreateBackup(string backupPath, List saves, List applications) - { - string mountName = "save"; - string outputMountName = "output"; - - foreach (ApplicationData application in applications) - { - try - { - //Register destination folder as output and mount output - Result registerOutpDirResult = RegisterOutputDirectory(Path.Combine(backupPath, application.TitleId), outputMountName); - if (registerOutpDirResult.IsFailure()) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not register and mount output directory."); - - } - - //Mount SaveData as save, opens the saveDataIterators and starts reading saveDataInfo - Result openAndReadSaveDataResult = OpenSaveDataIteratorAndReadSaveData(mountName, application, out SaveDataInfo saveDataInfo); - - if(openAndReadSaveDataResult.IsFailure()) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not open save Iterator and start reading for application: {application.TitleName}"); - } - else - { - //Copies the whole directory from save mount to output mount - Result copyDirResult = CopySaveDataDirectory(mountName, outputMountName); - } - - - //Unmount save and output - UnmountDirectory(saveDataInfo.ProgramId.Value, mountName); - UnmountDirectory(saveDataInfo.ProgramId.Value, outputMountName); - } - catch (Exception) - { - continue; - } - } - } - - private Result RegisterOutputDirectory(string backupPath, string mountName) - { - using UniqueRef outputFileSystem = new UniqueRef(new LibHac.FsSystem.LocalFileSystem(backupPath)); - - Result registerResult = _horizonClient.Fs.Register(mountName.ToU8Span(), ref outputFileSystem.Ref()); - if (registerResult.IsFailure()) return registerResult.Miss(); - - return registerResult; - } - - private Result GetSaveDataIterator(out SaveDataInfo saveDataInfo, ApplicationData application) - { - return _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, - SaveDataSpaceId.User, - SaveDataFilter.Make(ulong.Parse(application.TitleId, NumberStyles.HexNumber), - saveType: default, - _userId, - saveDataId: default, - index: default)); - } - - private Result MountSaveDataDirectory(ulong programId, string mountName) - { - U8Span mountNameu8 = mountName.ToU8Span(); - - if (!_horizonClient.Fs.IsMounted(mountNameu8)) - { - return _horizonClient.Fs.MountSaveData(mountNameu8, ConvertProgramIdToApplicationId(programId), _userId); - } - - return Result.Success; - } - - private Result UnmountDirectory(ulong programId, string mountName) - { - U8Span mountNameu8 = mountName.ToU8Span(); - - if (_horizonClient.Fs.IsMounted(mountNameu8)) - { - _horizonClient.Fs.Unmount(mountNameu8); - } - - return Result.Success; - } - - private Result OpenSaveDataIteratorAndReadSaveData(string mountName, ApplicationData application, out SaveDataInfo saveDataInfo) - { - Result getSvDataIteratorResult = GetSaveDataIterator(out saveDataInfo, application); - if (getSvDataIteratorResult.IsFailure()) return getSvDataIteratorResult; - - Result mountSvDataResult = MountSaveDataDirectory(saveDataInfo.ProgramId.Value, mountName); - if (mountSvDataResult.IsFailure()) return mountSvDataResult; - - UniqueRef saveDataIterator = new UniqueRef(); - Result openSvDataIteratorResult = _horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator, SaveDataSpaceId.User); - if (openSvDataIteratorResult.IsFailure()) return openSvDataIteratorResult; - - Result readSvDataInfoResult = saveDataIterator.Get.ReadSaveDataInfo(out long readCount, new Span(ref saveDataInfo)); - if (readSvDataInfoResult.IsFailure()) return readSvDataInfoResult; - - return Result.Success; - } - - private Result CopySaveDataDirectory(string sourcePath, string destPath) - { - Result openDir = _horizonClient.Fs.OpenDirectory(out DirectoryHandle dirHandle, $"{sourcePath}:/".ToU8Span(), OpenDirectoryMode.All); - if (openDir.IsFailure()) return openDir; - - using (dirHandle) - { - sourcePath = $"{sourcePath}:/"; - destPath = $"{destPath}:/"; - foreach (DirectoryEntryEx entry in _horizonClient.Fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) - { - string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); - - if (entry.Type == DirectoryEntryType.Directory) - { - Result copyDirResult = CopyDirectory(subSrcPath, subDstPath); - } - if (entry.Type == DirectoryEntryType.File) - { - Result copyFinalDir = CopyFile(subSrcPath, subDstPath, entry.Size); - } - } - - _horizonClient.Fs.CloseDirectory(dirHandle); - - return Result.Success; - } - } - - private Result CopyDirectory(string subSrcPath, string subDstPath) - { - _horizonClient.Fs.EnsureDirectoryExists(subDstPath); - - Result copyDirResult = _horizonClient.Fs.CopyDirectory(subSrcPath, subDstPath); - - if (copyDirResult.IsFailure()) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not copy directory: \n{subSrcPath} to destination {subDstPath}"); - } - - Logger.Info.Value.Print(LogClass.Application, $"Successfully copied directory: \n{subSrcPath} to destination {subDstPath}"); - - return copyDirResult; - } - - private Result CopyFile(string subSrcPath, string subDstPath, long entrySize) - { - _horizonClient.Fs.CreateOrOverwriteFile(subDstPath, entrySize); - - Result copyFileResult = _horizonClient.Fs.CopyFile(subSrcPath, subDstPath); - if (copyFileResult.IsFailure()) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not copy file: \n{subSrcPath} to destination '{subDstPath}"); - } - - Logger.Info.Value.Print(LogClass.Application, $"Successfully copied file: \n{subSrcPath} to destination {subDstPath}"); - - return copyFileResult; - } - - private LibHac.Ncm.ApplicationId ConvertProgramIdToApplicationId(ulong programId) - { - return new LibHac.Ncm.ApplicationId(programId); + saveDataFileManager.SaveUserSaveDirectoryAsZip(backupFolder, saves); } private async Task GetAndPrepareBackupPath(MainWindow mainWindow) diff --git a/Ryujinx.Ava/Ui/Controls/SaveDataImporter.cs b/Ryujinx.Ava/Ui/Controls/SaveDataImporter.cs index e1fb30bfd..c818bb46a 100644 --- a/Ryujinx.Ava/Ui/Controls/SaveDataImporter.cs +++ b/Ryujinx.Ava/Ui/Controls/SaveDataImporter.cs @@ -1,43 +1,39 @@ using Avalonia.Controls; using LibHac; +using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.Windows; -using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; -using System; +using Ryujinx.Ui.App.Common; using System.Collections.Generic; -using System.IO; -using System.IO.Compression; +using System.Globalization; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Path = System.IO.Path; +using UserId = LibHac.Fs.UserId; +using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile; namespace Ryujinx.Ava.Ui.Controls { internal class SaveDataImporter { + private readonly SaveDataFileManager saveDataFileManager; - private readonly UserProfile _userProfile; - private readonly HorizonClient _horizonClient; - private readonly VirtualFileSystem _virtualFileSystem; - - private async Task ShowConditionMessage() + public SaveDataImporter(List applications, UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem) { - return await ContentDialogHelper.CreateChoiceDialog("Restore Backup", - "You have to start every game at least once to create a save directory for the game before you can Restore the backup save data!", - "Do you want to continue?"); + UserId userId = new UserId( + ulong.Parse(userProfile.UserId.High.ToString(), NumberStyles.HexNumber), + ulong.Parse(userProfile.UserId.Low.ToString(), NumberStyles.HexNumber)); + + saveDataFileManager = new SaveDataFileManager(applications, userProfile, horizonClient, virtualFileSystem, userId); } public async void RestoreSavedataBackup(MainWindow mainWindow) { - if (!(await ShowConditionMessage())) return; - string[] backupZipFiles = await ShowFolderDialog(mainWindow); - ExtractBackupToSaveDirectory(backupZipFiles); + //Single because we set AllowMultiple = False in folder dialog options and want only one backup file. => Our export + saveDataFileManager.RestoreSavedataBackup(backupZipFiles.Single()); Logger.Info.Value.Print(LogClass.Application, $"Done extracting savedata backup!", nameof(SaveDataImporter)); } @@ -53,79 +49,5 @@ namespace Ryujinx.Ava.Ui.Controls return await dialog.ShowAsync(mainWindow); } - - private Dictionary GetTitleIdWithSavedataPath(string saveDirectoryPath) - { - Dictionary titleIdWithSavePath = new Dictionary(); - - //Loop through all ExtraData0 files in the savedata directory and read the first 8 bytes to determine which game this belongs to - foreach (var saveDataExtra0file in Directory.GetFiles(saveDirectoryPath, "ExtraData0*", SearchOption.AllDirectories)) - { - try - { - string hexValues = FlipHexBytes(new string(Convert.ToHexString(File.ReadAllBytes(saveDataExtra0file)).Substring(0, 16).Reverse().ToArray())); - - if (!titleIdWithSavePath.ContainsKey(hexValues)) - { - titleIdWithSavePath.Add(hexValues, saveDataExtra0file); - } - } - catch (Exception) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not extract hex from savedata file: {saveDataExtra0file}", nameof(SaveDataImporter)); - } - } - - return titleIdWithSavePath; - } - - private string FlipHexBytes(string hexString) - { - StringBuilder result = new StringBuilder(); - - for (int i = 0; i <= hexString.Length - 2; i = i + 2) - { - result.Append(new StringBuilder(new string(hexString.Substring(i, 2).Reverse().ToArray()))); - } - - return result.ToString(); - } - - private void ExtractBackupToSaveDirectory(string[] backupZipFiles) - { - if (!string.IsNullOrWhiteSpace(backupZipFiles.First()) && File.Exists(backupZipFiles.First())) - { - string tempZipExtractionPath = Path.GetTempPath(); - ZipFile.ExtractToDirectory(backupZipFiles.First(), tempZipExtractionPath, true); - - Logger.Info.Value.Print(LogClass.Application, $"Extracted Backup zip to temp path: {tempZipExtractionPath}", nameof(SaveDataImporter)); - - string saveDir = Path.Combine(AppDataManager.BaseDirPath, AppDataManager.DefaultNandDir, "user", "save"); - - Dictionary titleIdsAndSavePaths = GetTitleIdWithSavedataPath(saveDir); - Dictionary titleIdsAndBackupPaths = GetTitleIdWithSavedataPath(tempZipExtractionPath); - - ReplaceSavedataFiles(titleIdsAndSavePaths, titleIdsAndBackupPaths); - } - } - - private void ReplaceSavedataFiles(Dictionary titleIdsWithSavePaths, Dictionary titleIdsAndBackupPaths) - { - foreach (var titleIdAndBackupPath in titleIdsAndBackupPaths) - { - if (titleIdsWithSavePaths.ContainsKey(titleIdAndBackupPath.Key)) - { - try - { - Directory.Move(Directory.GetParent(titleIdAndBackupPath.Value).FullName, Directory.GetParent(titleIdsWithSavePaths[titleIdAndBackupPath.Key]).FullName); - Logger.Info.Value.Print(LogClass.Application, $"Copied Savedata {titleIdAndBackupPath.Value} to {titleIdsWithSavePaths[titleIdAndBackupPath.Key]}", nameof(SaveDataImporter)); - } - catch (Exception) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not copy Savedata {titleIdAndBackupPath.Value} to {titleIdsWithSavePaths[titleIdAndBackupPath.Key]}", nameof(SaveDataImporter)); - } - } - } - } } } \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs b/Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs index de5084c1f..e0a66cd45 100644 --- a/Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs +++ b/Ryujinx.Ava/Ui/Controls/SaveManager.axaml.cs @@ -91,15 +91,16 @@ namespace Ryujinx.Ava.Ui.Controls public void ImportBackup() { - SaveDataImporter restoreSavedataCommand = new SaveDataImporter(); + SaveDataImporter restoreSavedataCommand = new SaveDataImporter(_applications, _userProfile, _horizonClient, _virtualFileSystem); restoreSavedataCommand.RestoreSavedataBackup(Parent.VisualRoot as MainWindow); + + LoadSaves(); } public void ExportBackup() { - SaveDataExporter backupSavedataCommand = new SaveDataExporter(_userProfile, _horizonClient, _virtualFileSystem); - backupSavedataCommand.SaveUserSaveDirectoryAsZip(Parent.VisualRoot as MainWindow, Saves.ToList(), _applications); - + SaveDataExporter backupSavedataCommand = new SaveDataExporter(_applications, _userProfile, _horizonClient, _virtualFileSystem); + backupSavedataCommand.SaveUserSaveDirectoryAsZip(Parent.VisualRoot as MainWindow, Saves.ToList()); } public void LoadSaves() diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs index f9bf68792..b35e1dce1 100644 --- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs @@ -944,58 +944,6 @@ namespace Ryujinx.Ava.Ui.ViewModels OpenHelper.OpenFolder(logPath); } - public static void OpenGlobalDlcsFolder() - { - string globalDlcsPath = Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalDlcs"); - - new DirectoryInfo(globalDlcsPath).Create(); - - OpenHelper.OpenFolder(globalDlcsPath); - } - - public static void OpenGlobalUpdatesFolder() - { - string golbalUpdatesPath = Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalUpdates"); - - new DirectoryInfo(golbalUpdatesPath).Create(); - - OpenHelper.OpenFolder(golbalUpdatesPath); - } - - public async void LoadGlobalDlcs() - { - AutoDownloadableContentLoader downloadableContentManager = new AutoDownloadableContentLoader(Applications, _owner.VirtualFileSystem); - - //Searches for the files in the Global Dlcs folder and puts their path and titlename (from folder) in a dictionary. - Dictionary dlcsAndUpdatesPathWithGameName = Directory.GetFiles(Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalDlcs")) - .ToDictionary(x => x, y => Path.GetFileName(y).Split(new[] { "[" }, StringSplitOptions.RemoveEmptyEntries) - .First() - .Trim()); - - - foreach (ApplicationData application in Applications) - { - await downloadableContentManager.AutoLoadDlcsAsync(application, dlcsAndUpdatesPathWithGameName); - } - } - - public async void LoadGlobalUpdates() - { - AutoTitleUpdateLoader titleUpdateManager = new AutoTitleUpdateLoader(Applications, _owner.VirtualFileSystem); - - //Searches for the files in the Global Updates folder and puts their path and titlename (from folder) in a dictionary. - Dictionary dlcsAndUpdatesPathWithGameName = Directory.GetFiles(Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "GlobalUpdates")) - .ToDictionary(x => x, y => Path.GetFileName(y).Split(new[] { "[" }, StringSplitOptions.RemoveEmptyEntries) - .First() - .Trim()); - - - foreach (ApplicationData application in Applications) - { - await titleUpdateManager.AutoLoadUpdatesAsync(application, dlcsAndUpdatesPathWithGameName); - } - } - public void ToggleFullscreen() { if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs) diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml index a39876c57..ba936a2c3 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml @@ -104,24 +104,6 @@ Header="{locale:Locale MenuBarFileOpenLogsFolder}" ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" /> - - - - - -