added language support

new method for backup and restoring save data
user can choose filename now
This commit is contained in:
Akisuke 2022-12-13 23:33:19 +01:00
parent fb3c282544
commit 34718e7aa7
10 changed files with 105 additions and 63 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "テクスチャの再圧縮を有効",

View file

@ -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",

View file

@ -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:"

View file

@ -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": "启用纹理重压缩",

View file

@ -542,6 +542,7 @@
"CompilingShaders": "編譯渲染器中",
"AllKeyboards": "所有鍵盤",
"OpenFileDialogTitle": "選擇支援的檔案格式",
"CreateZipFileDialogTitle": "選擇目錄和文件名",
"OpenFolderDialogTitle": "選擇一個包含解包遊戲的資料夾",
"AllSupportedFormats": "全部支援的格式",
"RyujinxUpdater": "Ryujinx 更新程式",

View file

@ -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<string> OpenFolderDialog()
private async Task<string> 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<FileDialogFilter>(new[] { new FileDialogFilter() { Extensions = new System.Collections.Generic.List<string>() { "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);
Logger.Info.Value.Print(LogClass.Application, $"Start creating backup...", 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));
}
ZipFile.CreateFromDirectory(saveDir, zipFolderPath);
Logger.Info.Value.Print(LogClass.Application, $"Backup done. Zip is locate under {directoryPath}", nameof(BackupSavedataCommand));
}
}
}

View file

@ -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<FileDialogFilter>(new[] { new FileDialogFilter() { Extensions = new List<string>() { "zip" } } })
};
return await dialog.ShowAsync(parentControl.VisualRoot as MainWindow);
}
private Dictionary<string, string> GetTitleIdWithSavedataPath(string saveDirectoryPath)
{
Dictionary<string, string> titleIdWithSavePath = new Dictionary<string, string>();
//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<string, string> titleIdsAndSavePaths = GetTitleIdWithSavedataPath(saveDir);
Dictionary<string, string> titleIdsAndBackupPaths = GetTitleIdWithSavedataPath(tempZipExtractionPath);
ReplaceSavedataFiles(titleIdsAndSavePaths, titleIdsAndBackupPaths);
}
}
private void ReplaceSavedataFilesWithBackupSaveFiles(string[] backupSavedataPath, string saveDirectory)
private void ReplaceSavedataFiles(Dictionary<string, string> titleIdsWithSavePaths, Dictionary<string, string> 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();
}
}
}