diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json index d2b3b6587..aa25898f8 100644 --- a/Ryujinx.Ava/Assets/Locales/de_DE.json +++ b/Ryujinx.Ava/Assets/Locales/de_DE.json @@ -477,7 +477,7 @@ "GridSizeTooltip": "Ändert die Größe der Rasterelemente", "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Brasilianisches Portugiesisch", "AboutRyujinxContributorsButtonHeader": "Alle Mitwirkenden anzeigen", - "SettingsTabSystemAudioVolume" : "Lautstärke: ", + "SettingsTabSystemAudioVolume": "Lautstärke: ", "AudioVolumeTooltip": "Ändert die Lautstärke", "SettingsTabSystemEnableInternetAccess": "Gast-Internet-Zugang/LAN Modus", "EnableInternetAccessTooltip": "Erlaubt es der emulierten Anwendung sich mit dem Internet zu verbinden.\n\nSpiele die den LAN-Modus unterstützen, ermöglichen es Ryujinx sich sowohl mit anderen Ryujinx-Systemen, als auch mit offiziellen Nintendo Switch Konsolen zu verbinden. Allerdings nur, wenn diese Option aktiviert ist und die Systeme mit demselben lokalen Netzwerk verbunden sind.\n\nDies erlaubt KEINE Verbindung zu Nintendo-Servern. Kann bei bestimmten Spielen die versuchen sich mit dem Internet zu verbinden zum Absturz führen.\n\nIm Zweifelsfall AUS lassen", @@ -542,6 +542,7 @@ "CompilingShaders": "Shader werden kompiliert", "AllKeyboards": "Alle Tastaturen", "OpenFileDialogTitle": "Wähle eine unterstützte Datei", + "CreateZipFileDialogTitle": "Wählen Sie ein Verzeichnis und einen Dateinamen aus", "OpenFolderDialogTitle": "Wähle einen Ordner mit einem entpackten Spiel", "AllSupportedFormats": "Alle unterstützten Formate", "RyujinxUpdater": "Ryujinx - Updater", @@ -554,8 +555,8 @@ "SettingsTabHotkeysToggleMuteHotkey": "Stummschalten:", "ControllerMotionTitle": "Bewegungssteuerung - Einstellungen", "ControllerRumbleTitle": "Vibration - Einstellungen", - "SettingsSelectThemeFileDialogTitle" : "Wähle ein Design für die Emulator-Benutzeroberfläche", - "SettingsXamlThemeFile" : "Xaml Design-Datei", + "SettingsSelectThemeFileDialogTitle": "Wähle ein Design für die Emulator-Benutzeroberfläche", + "SettingsXamlThemeFile": "Xaml Design-Datei", "AvatarWindowTitle": "Profile verwalten - Avatar", "Amiibo": "Amiibo", "Unknown": "Unbekannt", @@ -575,12 +576,12 @@ "Discard": "Verwerfen", "UserProfilesSetProfileImage": "Profilbild einrichten", "UserProfileEmptyNameError": "Name ist erforderlich", - "UserProfileNoImageError": "Bitte ein Profilbild auswählen", + "UserProfileNoImageError": "Bitte ein Profilbild auswählen", "GameUpdateWindowHeading": "Update verfügbar für {0} [{1}]", "SettingsTabHotkeysResScaleUpHotkey": "Auflösung erhöhen:", "SettingsTabHotkeysResScaleDownHotkey": "Auflösung verringern:", "UserProfilesName": "Name:", - "UserProfilesUserId" : "Benutzer Id:", + "UserProfilesUserId": "Benutzer Id:", "SettingsTabGraphicsBackend": "Grafik-Backend:", "SettingsTabGraphicsBackendTooltip": "Verwendendetes Grafik-Backend", "SettingsEnableTextureRecompression": "Textur-Rekompression", diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index 732d524d3..5778fd0c2 100644 --- a/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/Ryujinx.Ava/Assets/Locales/en_US.json @@ -545,6 +545,7 @@ "CompilingShaders": "Compiling Shaders", "AllKeyboards": "All keyboards", "OpenFileDialogTitle": "Select a supported file to open", + "CreateZipFileDialogTitle": "Select a directory and filename", "OpenFolderDialogTitle": "Select a folder with an unpacked game", "AllSupportedFormats": "All Supported Formats", "RyujinxUpdater": "Ryujinx Updater", diff --git a/Ryujinx.Ava/Assets/Locales/es_ES.json b/Ryujinx.Ava/Assets/Locales/es_ES.json index d095e5ea1..ac0660bf6 100644 --- a/Ryujinx.Ava/Assets/Locales/es_ES.json +++ b/Ryujinx.Ava/Assets/Locales/es_ES.json @@ -476,7 +476,7 @@ "GridSize": "Tamaño de cuadrícula", "GridSizeTooltip": "Cambia el tamaño de los objetos en la cuadrícula", "SettingsTabSystemSystemLanguageBrazilianPortuguese": "Portugués brasileño", - "AboutRyujinxContributorsButtonHeader": "Ver todos los contribuidores", + "AboutRyujinxContributorsButtonHeader": "Ver todos los contribuidores", "SettingsTabSystemAudioVolume": "Volumen: ", "AudioVolumeTooltip": "Ajusta el nivel de volumen", "SettingsTabSystemEnableInternetAccess": "Conectar guest a Internet/Modo LAN", @@ -542,6 +542,7 @@ "CompilingShaders": "Compilando sombreadores", "AllKeyboards": "Todos los teclados", "OpenFileDialogTitle": "Selecciona un archivo soportado para cargar", + "CreateZipFileDialogTitle": "Seleccione un directorio y un nombre de archivo", "OpenFolderDialogTitle": "Selecciona una carpeta con un juego desempaquetado", "AllSupportedFormats": "Todos los formatos soportados", "RyujinxUpdater": "Actualizador de Ryujinx", diff --git a/Ryujinx.Ava/Assets/Locales/ja_JP.json b/Ryujinx.Ava/Assets/Locales/ja_JP.json index 571f098f6..f32d6a198 100644 --- a/Ryujinx.Ava/Assets/Locales/ja_JP.json +++ b/Ryujinx.Ava/Assets/Locales/ja_JP.json @@ -542,6 +542,7 @@ "CompilingShaders": "シェーダをコンパイル中", "AllKeyboards": "すべてのキーボード", "OpenFileDialogTitle": "開くファイルを選択", + "CreateZipFileDialogTitle": "ディレクトリとファイル名を選択", "OpenFolderDialogTitle": "展開されたゲームフォルダを選択", "AllSupportedFormats": "すべての対応フォーマット", "RyujinxUpdater": "Ryujinx アップデータ", @@ -575,12 +576,12 @@ "Discard": "破棄", "UserProfilesSetProfileImage": "プロファイル画像を設定", "UserProfileEmptyNameError": "名称が必要です", - "UserProfileNoImageError": "プロファイル画像が必要です", + "UserProfileNoImageError": "プロファイル画像が必要です", "GameUpdateWindowHeading": "利用可能なアップデート {0} [{1}]", "SettingsTabHotkeysResScaleUpHotkey": "解像度を上げる:", "SettingsTabHotkeysResScaleDownHotkey": "解像度を下げる:", "UserProfilesName": "名称:", - "UserProfilesUserId" : "ユーザID:", + "UserProfilesUserId": "ユーザID:", "SettingsTabGraphicsBackend": "グラフィックスバックエンド", "SettingsTabGraphicsBackendTooltip": "使用するグラフィックスバックエンドです", "SettingsEnableTextureRecompression": "テクスチャの再圧縮を有効", diff --git a/Ryujinx.Ava/Assets/Locales/pl_PL.json b/Ryujinx.Ava/Assets/Locales/pl_PL.json index 7a376be7d..a8d150e89 100644 --- a/Ryujinx.Ava/Assets/Locales/pl_PL.json +++ b/Ryujinx.Ava/Assets/Locales/pl_PL.json @@ -542,6 +542,7 @@ "CompilingShaders": "Kompilowanie Shaderów", "AllKeyboards": "Wszystkie klawiatury", "OpenFileDialogTitle": "Wybierz obsługiwany plik do otwarcia", + "CreateZipFileDialogTitle": "Wybierz katalog i nazwę pliku", "OpenFolderDialogTitle": "Wybierz folder z rozpakowaną grą", "AllSupportedFormats": "Wszystkie Obsługiwane Formaty", "RyujinxUpdater": "Aktualizator Ryujinx", @@ -575,12 +576,12 @@ "Discard": "Odrzuć", "UserProfilesSetProfileImage": "Ustaw Obraz Profilu", "UserProfileEmptyNameError": "Nazwa jest wymagana", - "UserProfileNoImageError": "Należy ustawić obraz profilowy", + "UserProfileNoImageError": "Należy ustawić obraz profilowy", "GameUpdateWindowHeading": "Aktualizacje Dostępne dla {0} [{1}]", "SettingsTabHotkeysResScaleUpHotkey": "Zwiększ Rozdzielczość:", "SettingsTabHotkeysResScaleDownHotkey": "Zmniejsz Rozdzielczość:", "UserProfilesName": "Nazwa:", - "UserProfilesUserId" : "ID Użytkownika:", + "UserProfilesUserId": "ID Użytkownika:", "SettingsTabGraphicsBackend": "Backend Graficzny", "SettingsTabGraphicsBackendTooltip": "Używalne Backendy Graficzne", "SettingsEnableTextureRecompression": "Włącz Rekompresję Tekstur", diff --git a/Ryujinx.Ava/Assets/Locales/tr_TR.json b/Ryujinx.Ava/Assets/Locales/tr_TR.json index 3f7baca74..fbb2fabfa 100644 --- a/Ryujinx.Ava/Assets/Locales/tr_TR.json +++ b/Ryujinx.Ava/Assets/Locales/tr_TR.json @@ -542,6 +542,7 @@ "CompilingShaders": "Shaderlar Derleniyor", "AllKeyboards": "Tüm Klavyeler", "OpenFileDialogTitle": "Açmak için desteklenen bir dosya seçin", + "CreateZipFileDialogTitle": "Bir dizin ve dosya adı seçin", "OpenFolderDialogTitle": "Ayrıştırılmamış oyun içeren bir klasör seçin", "AllSupportedFormats": "Tüm Desteklenen Formatlar", "RyujinxUpdater": "Ryujinx Güncelleyicisi", @@ -575,7 +576,7 @@ "Discard": "Iskarta", "UserProfilesSetProfileImage": "Profil Resmi Ayarla", "UserProfileEmptyNameError": "İsim gerekli", - "UserProfileNoImageError": "Profil resmi ayarlanmalıdır", + "UserProfileNoImageError": "Profil resmi ayarlanmalıdır", "GameUpdateWindowHeading": "{0} için güncellemeler mevcut [{1}]", "SettingsTabHotkeysResScaleUpHotkey": "Çözünürlüğü artır:", "SettingsTabHotkeysResScaleDownHotkey": "Çözünürlüğü azalt:" diff --git a/Ryujinx.Ava/Assets/Locales/zh_CN.json b/Ryujinx.Ava/Assets/Locales/zh_CN.json index 09c89e4e8..d77ec4c45 100644 --- a/Ryujinx.Ava/Assets/Locales/zh_CN.json +++ b/Ryujinx.Ava/Assets/Locales/zh_CN.json @@ -542,6 +542,7 @@ "CompilingShaders": "编译着色器中", "AllKeyboards": "所有键盘", "OpenFileDialogTitle": "选择支持的文件格式", + "CreateZipFileDialogTitle": "选择目录和文件名", "OpenFolderDialogTitle": "选择一个包含解包游戏的文件夹", "AllSupportedFormats": "所有支持的格式", "RyujinxUpdater": "Ryujinx 更新程序", @@ -575,12 +576,12 @@ "Discard": "返回", "UserProfilesSetProfileImage": "选择头像", "UserProfileEmptyNameError": "必须输入名称", - "UserProfileNoImageError": "请选择您的头像", + "UserProfileNoImageError": "请选择您的头像", "GameUpdateWindowHeading": "适用于 {0} [{1}] 的更新", "SettingsTabHotkeysResScaleUpHotkey": "提高分辨率:", "SettingsTabHotkeysResScaleDownHotkey": "降低分辨率:", "UserProfilesName": "名称:", - "UserProfilesUserId" : "用户 ID:", + "UserProfilesUserId": "用户 ID:", "SettingsTabGraphicsBackend": "图形后端", "SettingsTabGraphicsBackendTooltip": "显卡使用的图形后端", "SettingsEnableTextureRecompression": "启用纹理重压缩", diff --git a/Ryujinx.Ava/Assets/Locales/zh_TW.json b/Ryujinx.Ava/Assets/Locales/zh_TW.json index 856e04599..d53fc27e4 100644 --- a/Ryujinx.Ava/Assets/Locales/zh_TW.json +++ b/Ryujinx.Ava/Assets/Locales/zh_TW.json @@ -542,6 +542,7 @@ "CompilingShaders": "編譯渲染器中", "AllKeyboards": "所有鍵盤", "OpenFileDialogTitle": "選擇支援的檔案格式", + "CreateZipFileDialogTitle": "選擇目錄和文件名", "OpenFolderDialogTitle": "選擇一個包含解包遊戲的資料夾", "AllSupportedFormats": "全部支援的格式", "RyujinxUpdater": "Ryujinx 更新程式", diff --git a/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs b/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs index 3b4f78e61..2975f2c46 100644 --- a/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs +++ b/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs @@ -1,5 +1,5 @@ -using Avalonia.Controls; -using Avalonia.Logging; + +using Avalonia.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common.Configuration; @@ -17,6 +17,7 @@ namespace Ryujinx.Ava.Ui.Controls internal class BackupSavedataCommand : ICommand { public event EventHandler CanExecuteChanged; + private readonly IControl parentControl; public BackupSavedataCommand(IControl parentControl) @@ -36,37 +37,46 @@ namespace Ryujinx.Ava.Ui.Controls public async void SaveUserSaveDirectoryAsZip() { - CreateBackupZip(await OpenFolderDialog()); + CreateBackupZip(await GetAndPrepareBackupPath()); } - private async Task OpenFolderDialog() + private async Task GetAndPrepareBackupPath() { - OpenFolderDialog dialog = new() + SaveFileDialog saveFileDialog = new SaveFileDialog() { - Title = LocaleManager.Instance["OpenFolderDialogTitle"] + Title = LocaleManager.Instance["CreateZipFileDialogTitle"], + InitialFileName = "Ryujinx_backup.zip", + Filters = new System.Collections.Generic.List(new[] { new FileDialogFilter() { Extensions = new System.Collections.Generic.List() { "zip" } } }) }; - return await dialog.ShowAsync(parentControl.VisualRoot as MainWindow); + string zipPath = await saveFileDialog.ShowAsync(parentControl.VisualRoot as MainWindow); + + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + return zipPath; } - private void CreateBackupZip(string directoryPath) + private void CreateBackupZip(string userBackupPath) { - if (!string.IsNullOrWhiteSpace(directoryPath) && Directory.Exists(directoryPath)) + if (!string.IsNullOrWhiteSpace(userBackupPath) && Directory.Exists(Directory.GetParent(userBackupPath).FullName)) { string saveDir = Path.Combine(AppDataManager.BaseDirPath, AppDataManager.DefaultNandDir, "user", "save"); - string zipFolderPath = Path.Combine(directoryPath, "Ryujinx_backup.zip"); - - Logger.Info.Value.Print(LogClass.Application, $"Start creating backup...", nameof(BackupSavedataCommand)); - - if (File.Exists(zipFolderPath)) + try { - File.Delete(zipFolderPath); - } - - ZipFile.CreateFromDirectory(saveDir, zipFolderPath); + Logger.Info.Value.Print(LogClass.Application, $"Start creating backup...", nameof(BackupSavedataCommand)); - Logger.Info.Value.Print(LogClass.Application, $"Backup done. Zip is locate under {directoryPath}", nameof(BackupSavedataCommand)); + ZipFile.CreateFromDirectory(saveDir, userBackupPath); + + Logger.Info.Value.Print(LogClass.Application, $"Backup done. Zip is locate under {userBackupPath}", nameof(BackupSavedataCommand)); + } + catch (Exception) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not create backup zip file.", nameof(BackupSavedataCommand)); + } } } } diff --git a/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs b/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs index 473b24cd1..f8575f5c4 100644 --- a/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs +++ b/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs @@ -4,9 +4,11 @@ using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Windows.Input; using Path = System.IO.Path; @@ -58,11 +60,49 @@ namespace Ryujinx.Ava.Ui.Controls { Title = LocaleManager.Instance["OpenFileDialogTitle"], AllowMultiple = false, + Filters = new List(new[] { new FileDialogFilter() { Extensions = new List() { "zip" } } }) }; return await dialog.ShowAsync(parentControl.VisualRoot as 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(RestoreSavedataCommand)); + } + } + + 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())) @@ -73,47 +113,31 @@ namespace Ryujinx.Ava.Ui.Controls Logger.Info.Value.Print(LogClass.Application, $"Extracted Backup zip to temp path: {tempZipExtractionPath}", nameof(RestoreSavedataCommand)); string saveDir = Path.Combine(AppDataManager.BaseDirPath, AppDataManager.DefaultNandDir, "user", "save"); - ReplaceSavedataFilesWithBackupSaveFiles(Directory.GetDirectories(tempZipExtractionPath), saveDir); + + Dictionary titleIdsAndSavePaths = GetTitleIdWithSavedataPath(saveDir); + Dictionary titleIdsAndBackupPaths = GetTitleIdWithSavedataPath(tempZipExtractionPath); + + ReplaceSavedataFiles(titleIdsAndSavePaths, titleIdsAndBackupPaths); } } - private void ReplaceSavedataFilesWithBackupSaveFiles(string[] backupSavedataPath, string saveDirectory) + private void ReplaceSavedataFiles(Dictionary titleIdsWithSavePaths, Dictionary titleIdsAndBackupPaths) { - //All current save files for later replacement - string[] userSaveFiles = GetSaveFilesInAllSubDirectories(saveDirectory); - - //Loops through every Title save folder and replaces all user save data files with the savedata files inside the extracted backup folder - //Logic to decide wich file is replaces is based on der filename and parent directory - foreach (string[] backupSaveFiles in backupSavedataPath.Select(GetSaveFilesInAllSubDirectories)) + foreach (var titleIdAndBackupPath in titleIdsAndBackupPaths) { - foreach (string backupSaveFile in backupSaveFiles) + if (titleIdsWithSavePaths.ContainsKey(titleIdAndBackupPath.Key)) { - foreach (string userSaveFile in GetSaveFilesWithSameNameAndParentDir(userSaveFiles, backupSaveFile)) + try { - try - { - File.Copy(backupSaveFile, userSaveFile, true); - Logger.Info.Value.Print(LogClass.Application, $"Copied Savedata {backupSaveFile} to {userSaveFile}", nameof(RestoreSavedataCommand)); - } - catch (Exception) - { - Logger.Error.Value.Print(LogClass.Application, $"Could not copy Savedata {backupSaveFile} to {userSaveFile}", nameof(RestoreSavedataCommand)); - } + 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(RestoreSavedataCommand)); + } + catch (Exception) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not copy Savedata {titleIdAndBackupPath.Value} to {titleIdsWithSavePaths[titleIdAndBackupPath.Key]}", nameof(RestoreSavedataCommand)); } } } } - - private string[] GetSaveFilesWithSameNameAndParentDir(string[] userSaveFiles, string backupSaveFile) - { - return userSaveFiles.Where(sf => Path.GetFileName(sf) == Path.GetFileName(backupSaveFile) && Directory.GetParent(sf).Name == Directory.GetParent(backupSaveFile).Name).ToArray(); - } - - private string[] GetSaveFilesInAllSubDirectories(string rootDirectory) - { - string[] unnecessarySaveFiles = { ".lock", "ExtraData0", "ExtraData1" }; - - return Directory.GetFiles(rootDirectory, "*", SearchOption.AllDirectories).Where(file => !unnecessarySaveFiles.Any(usf => file.EndsWith(usf))).ToArray(); - } } } \ No newline at end of file