diff --git a/Directory.Packages.props b/Directory.Packages.props
index 17d5fca83..d711b204e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,11 +3,11 @@
true
-
+
-
-
-
+
+
+
diff --git a/assets/Locales/Amiibo.json b/assets/Locales/Amiibo.json
new file mode 100644
index 000000000..9d85dcff3
--- /dev/null
+++ b/assets/Locales/Amiibo.json
@@ -0,0 +1,229 @@
+{
+ "Locales": [
+ {
+ "ID": "WindowTitle",
+ "Translations": {
+ "ar_SA": "أميبو",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Amiibo",
+ "es_ES": null,
+ "fr_FR": null,
+ "he_IL": "אמיבו",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": null,
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": null,
+ "ru_RU": null,
+ "sv_SE": null,
+ "th_TH": "อมิโบ้",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": null,
+ "zh_TW": null
+ }
+ },
+ {
+ "ID": "SeriesLabel",
+ "Translations": {
+ "ar_SA": "سلسلة",
+ "de_DE": "Serie",
+ "el_GR": "Σειρά",
+ "en_US": "Series",
+ "es_ES": "Serie",
+ "fr_FR": "Séries",
+ "he_IL": "סדרה",
+ "it_IT": "Serie",
+ "ja_JP": "シリーズ",
+ "ko_KR": "시리즈",
+ "no_NO": "Serie",
+ "pl_PL": "Seria",
+ "pt_BR": "Série",
+ "ru_RU": "Серия",
+ "sv_SE": "Serie",
+ "th_TH": "ซีรีส์",
+ "tr_TR": "Seri",
+ "uk_UA": "Серія",
+ "zh_CN": "系列",
+ "zh_TW": "系列"
+ }
+ },
+ {
+ "ID": "ShowAllAmiiboLabel",
+ "Translations": {
+ "ar_SA": "إظهار الكل Amiibo",
+ "de_DE": "Alle Amiibo anzeigen",
+ "el_GR": "Εμφάνιση Όλων των Amiibo",
+ "en_US": "Show All Amiibo",
+ "es_ES": "Mostrar Todas las Amiibo",
+ "fr_FR": "Afficher Tous les Amiibo",
+ "he_IL": "הצג הכל Amiibo",
+ "it_IT": "Mostra tutti gli Amiibo",
+ "ja_JP": "すべてのAmiiboを表示",
+ "ko_KR": "모든 Amiibo 표시",
+ "no_NO": "Vis alle Amiibo",
+ "pl_PL": "Pokaż Wszystkie Amiibo",
+ "pt_BR": "Mostrar Todos os Amiibo",
+ "ru_RU": "Показывать все Amiibo",
+ "sv_SE": "Visa alla Amiibo",
+ "th_TH": "แสดง Amiibo ทั้งหมด",
+ "tr_TR": "Tüm Amiibo'ları Göster",
+ "uk_UA": "Показати всі Amiibo",
+ "zh_CN": "显示所有 Amiibo",
+ "zh_TW": "顯示所有 Amiibo"
+ }
+ },
+ {
+ "ID": "UseRandomUuidLabel",
+ "Translations": {
+ "ar_SA": "استخدام UUID عشوائي",
+ "de_DE": "Zufällige UUID verwenden",
+ "el_GR": "Χρήση τυχαίου UUID",
+ "en_US": "Use Random UUID",
+ "es_ES": "Usar UUID aleatorio",
+ "fr_FR": "Utiliser un UUID aléatoire",
+ "he_IL": "השתמש ב-UUID אקראי",
+ "it_IT": "Usa UUID casuale",
+ "ja_JP": "ランダム UUID を使用",
+ "ko_KR": "무작위 UUID 사용",
+ "no_NO": "Bruk tilfeldig UUID",
+ "pl_PL": "Użyj losowego UUID",
+ "pt_BR": "Usar UUID aleatório",
+ "ru_RU": "Использовать случайный UUID",
+ "sv_SE": "Använd slumpmässig UUID",
+ "th_TH": "ใช้ UUID แบบสุ่ม",
+ "tr_TR": "Rastgele UUID kullan",
+ "uk_UA": "Використовувати випадковий UUID",
+ "zh_CN": "使用随机 UUID",
+ "zh_TW": "使用隨機 UUID"
+ }
+ },
+ {
+ "ID": "PauseEmulationWhileScanningAmiibo",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Emulation während des Scannens pausieren",
+ "el_GR": "",
+ "en_US": "Pause Emulation While Scanning",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "Приостанавливать эмуляцию во время сканирования",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "UsageLabel",
+ "Translations": {
+ "ar_SA": "الاستخدام:",
+ "de_DE": "Nutzung:",
+ "el_GR": "Χρήση:",
+ "en_US": "Usage:",
+ "es_ES": "Uso:",
+ "fr_FR": "Utilisation :",
+ "he_IL": "שימוש:",
+ "it_IT": "Utilizzo:",
+ "ja_JP": "使用法:",
+ "ko_KR": "사용법:",
+ "no_NO": "Forbruk:",
+ "pl_PL": "Użycie:",
+ "pt_BR": "Uso:",
+ "ru_RU": "Применение:",
+ "sv_SE": "Användning:",
+ "th_TH": "การใช้งาน:",
+ "tr_TR": "Kullanım:",
+ "uk_UA": "Використання:",
+ "zh_CN": "用法:",
+ "zh_TW": "用途:"
+ }
+ },
+ {
+ "ID": "WritableLabel",
+ "Translations": {
+ "ar_SA": "الاستخدام (يمكنه حفظ البيانات):",
+ "de_DE": "Nutzung (Kann Daten speichern):",
+ "el_GR": "Χρήση (Μπορεί να αποθηκεύσει δεδομένα):",
+ "en_US": "Usage (Can Save Data):",
+ "es_ES": "Uso (Puede guardar datos):",
+ "fr_FR": "Utilisation (Peut enregistrer des données) :",
+ "he_IL": "שימוש (יכול לשמור נתונים):",
+ "it_IT": "Utilizzo (Può salvare dati):",
+ "ja_JP": "使用法(データを保存できます):",
+ "ko_KR": "사용법 (데이터 저장 가능):",
+ "no_NO": "Bruk (Kan lagre data):",
+ "pl_PL": "Użycie (Może zapisywać dane):",
+ "pt_BR": "Uso (Pode salvar dados):",
+ "ru_RU": "Применение (Может сохранять данные):",
+ "sv_SE": "Användning (Kan spara data):",
+ "th_TH": "การใช้งาน (สามารถบันทึกข้อมูลได้):",
+ "tr_TR": "Kullanım (Veri kaydedebilir):",
+ "uk_UA": "Використання (Може зберігати дані):",
+ "zh_CN": "用法(可保存数据):",
+ "zh_TW": "用途(可儲存資料):"
+ }
+ },
+ {
+ "ID": "UnknownLabel",
+ "Translations": {
+ "ar_SA": "غير معروف",
+ "de_DE": "Unbekannt",
+ "el_GR": "Άγνωστο",
+ "en_US": "Unknown",
+ "es_ES": "Desconocido",
+ "fr_FR": "Inconnu",
+ "he_IL": "לא ידוע",
+ "it_IT": "Sconosciuto",
+ "ja_JP": "不明",
+ "ko_KR": "알 수 없음",
+ "no_NO": "Ukjent",
+ "pl_PL": "Nieznane",
+ "pt_BR": "Desconhecido",
+ "ru_RU": "Неизвестно",
+ "sv_SE": "Okänt",
+ "th_TH": "ไม่รู้จัก",
+ "tr_TR": "Bilinmeyen",
+ "uk_UA": "Невідомо",
+ "zh_CN": "未知",
+ "zh_TW": "未知"
+ }
+ },
+ {
+ "ID": "ScanButton",
+ "Translations": {
+ "ar_SA": "فحص Amiibo",
+ "de_DE": "Amiibo einscannen",
+ "el_GR": "Σαρώστε το Amiibo",
+ "en_US": "Scan Amiibo",
+ "es_ES": "Escanear Amiibo",
+ "fr_FR": "Scanner l'Amiibo",
+ "he_IL": "סרוק את ה-Amiibo",
+ "it_IT": "Scansiona l'Amiibo",
+ "ja_JP": "Amiiboをスキャン",
+ "ko_KR": "Amiibo 스캔하기",
+ "no_NO": "Skanne Amiibo",
+ "pl_PL": "Zeskanuj Amiibo",
+ "pt_BR": "Escanear Amiibo",
+ "ru_RU": "Сканировать Amiibo",
+ "sv_SE": "Skanna Amiibo",
+ "th_TH": "สแกน Amiibo",
+ "tr_TR": "Amiibo'yu tara",
+ "uk_UA": "Сканувати Amiibo",
+ "zh_CN": "扫描 Amiibo",
+ "zh_TW": "掃描 Amiibo"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Common_Buttons.json b/assets/Locales/Common_Buttons.json
new file mode 100644
index 000000000..1198c2cc6
--- /dev/null
+++ b/assets/Locales/Common_Buttons.json
@@ -0,0 +1,29 @@
+{
+ "Locales": [
+ {
+ "ID": "Cancel",
+ "Translations": {
+ "ar_SA": "إلغاء",
+ "de_DE": "Abbrechen",
+ "el_GR": "Ακύρωση",
+ "en_US": "Cancel",
+ "es_ES": "Cancelar",
+ "fr_FR": "Annuler",
+ "he_IL": "ביטול",
+ "it_IT": "Annulla",
+ "ja_JP": "キャンセル",
+ "ko_KR": "취소",
+ "no_NO": "Avbryt",
+ "pl_PL": "Anuluj",
+ "pt_BR": "Cancelar",
+ "ru_RU": "Отмена",
+ "sv_SE": "Avbryt",
+ "th_TH": "ยกเลิก",
+ "tr_TR": "İptal",
+ "uk_UA": "Скасувати",
+ "zh_CN": "取消",
+ "zh_TW": "取消"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Common_FilePicker.json b/assets/Locales/Common_FilePicker.json
new file mode 100644
index 000000000..751ca0847
--- /dev/null
+++ b/assets/Locales/Common_FilePicker.json
@@ -0,0 +1,29 @@
+{
+ "Locales": [
+ {
+ "ID": "AllSupportedFormats",
+ "Translations": {
+ "ar_SA": "كل التنسيقات المدعومة",
+ "de_DE": "Alle unterstützten Formate",
+ "el_GR": "Όλες Οι Υποστηριζόμενες Μορφές",
+ "en_US": "All Supported Formats",
+ "es_ES": "Todos los Formatos Soportados",
+ "fr_FR": "Tous les Formats Supportés",
+ "he_IL": "כל הפורמטים הנתמכים",
+ "it_IT": "Tutti i formati supportati",
+ "ja_JP": "すべての対応フォーマット",
+ "ko_KR": "지원되는 모든 형식",
+ "no_NO": "Alle støttede formater",
+ "pl_PL": "Wszystkie Obsługiwane Formaty",
+ "pt_BR": "Todos os Formatos Suportados",
+ "ru_RU": "Все поддерживаемые форматы",
+ "sv_SE": "Alla format som stöds",
+ "th_TH": "รูปแบบที่รองรับทั้งหมด",
+ "tr_TR": "Tüm Desteklenen Formatlar",
+ "uk_UA": "Усі підтримувані формати",
+ "zh_CN": "所有支持的格式",
+ "zh_TW": "所有支援的格式"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Common_Search.json b/assets/Locales/Common_Search.json
new file mode 100644
index 000000000..32900955a
--- /dev/null
+++ b/assets/Locales/Common_Search.json
@@ -0,0 +1,54 @@
+{
+ "Locales": [
+ {
+ "ID": "SearchAmiiboWatermark",
+ "Translations": {
+ "ar_SA": "بحث Amiibo...",
+ "de_DE": "Amiibo suchen...",
+ "el_GR": "Αναζήτηση Amiibo...",
+ "en_US": "Search Amiibo...",
+ "es_ES": "Buscar Amiibo...",
+ "fr_FR": "Rechercher Amiibo...",
+ "he_IL": "חפש Amiibo...",
+ "it_IT": "Cerca Amiibo...",
+ "ja_JP": "Amiiboを検索...",
+ "ko_KR": "Amiibo 검색...",
+ "no_NO": "Søk Amiibo...",
+ "pl_PL": "Wyszukaj Amiibo...",
+ "pt_BR": "Buscar Amiibo...",
+ "ru_RU": "Поиск Amiibo...",
+ "sv_SE": "Sök Amiibo...",
+ "th_TH": "ค้นหา Amiibo...",
+ "tr_TR": "Amiibo Ara...",
+ "uk_UA": "Пошук Amiibo...",
+ "zh_CN": "搜索 Amiibo...",
+ "zh_TW": "搜尋 Amiibo..."
+ }
+ },
+ {
+ "ID": "SearchWatermark",
+ "Translations": {
+ "ar_SA": "بحث",
+ "de_DE": "Suche",
+ "el_GR": "Αναζήτηση",
+ "en_US": "Search",
+ "es_ES": "Buscar",
+ "fr_FR": "Rechercher",
+ "he_IL": "חפש",
+ "it_IT": "Cerca",
+ "ja_JP": "検索",
+ "ko_KR": "찾기",
+ "no_NO": "Søk",
+ "pl_PL": "Wyszukaj",
+ "pt_BR": "Buscar",
+ "ru_RU": "Поиск",
+ "sv_SE": "Sök",
+ "th_TH": "ค้นหา",
+ "tr_TR": "Ara",
+ "uk_UA": "Пошук",
+ "zh_CN": "搜索",
+ "zh_TW": "搜尋"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Common_Sort.json b/assets/Locales/Common_Sort.json
new file mode 100644
index 000000000..63f229f4e
--- /dev/null
+++ b/assets/Locales/Common_Sort.json
@@ -0,0 +1,129 @@
+{
+ "Locales": [
+ {
+ "ID": "NameLabel",
+ "Translations": {
+ "ar_SA": "الاسم",
+ "de_DE": "",
+ "el_GR": "Όνομα",
+ "en_US": "Name",
+ "es_ES": "Nombre",
+ "fr_FR": "Nom",
+ "he_IL": "שם",
+ "it_IT": "Nome",
+ "ja_JP": "名称",
+ "ko_KR": "이름",
+ "no_NO": "Navn",
+ "pl_PL": "Nazwa",
+ "pt_BR": "Nome",
+ "ru_RU": "Название",
+ "sv_SE": "Namn",
+ "th_TH": "ชื่อ",
+ "tr_TR": "İsim",
+ "uk_UA": "Назва",
+ "zh_CN": "名称",
+ "zh_TW": "名稱"
+ }
+ },
+ {
+ "ID": "SavingsLabel",
+ "Translations": {
+ "ar_SA": "التوفير",
+ "de_DE": "Einsparung",
+ "el_GR": "Εξοικονόμηση",
+ "en_US": "Savings",
+ "es_ES": "Ahorro",
+ "fr_FR": "Économies",
+ "he_IL": "חיסכון",
+ "it_IT": "Risparmio",
+ "ja_JP": "節約",
+ "ko_KR": "절약",
+ "no_NO": "Besparelse",
+ "pl_PL": "Oszczędność",
+ "pt_BR": "Economia",
+ "ru_RU": "Экономия",
+ "sv_SE": "Besparing",
+ "th_TH": "การประหยัด",
+ "tr_TR": "Tasarruf",
+ "uk_UA": "Економія",
+ "zh_CN": "节省",
+ "zh_TW": "節省"
+ }
+ },
+ {
+ "ID": "TrimStatusLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmstatus",
+ "el_GR": "",
+ "en_US": "Trim Status",
+ "es_ES": "Estado del recorte",
+ "fr_FR": "État de réduction",
+ "he_IL": "",
+ "it_IT": "Stato della riduzione",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "Статус обрезки",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Статус обрізки",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "OrderAscending",
+ "Translations": {
+ "ar_SA": "تصاعدي",
+ "de_DE": "Aufsteigend",
+ "el_GR": "Αύξουσα",
+ "en_US": "Ascending",
+ "es_ES": "Ascendente",
+ "fr_FR": "Croissant",
+ "he_IL": "סדר עולה",
+ "it_IT": "Crescente",
+ "ja_JP": "昇順",
+ "ko_KR": "오름차순",
+ "no_NO": "Stigende",
+ "pl_PL": "Rosnąco",
+ "pt_BR": "Ascendente",
+ "ru_RU": "По Возрастанию",
+ "sv_SE": "Stigande",
+ "th_TH": "จากน้อยไปมาก",
+ "tr_TR": "Artan",
+ "uk_UA": "За зростанням",
+ "zh_CN": "升序",
+ "zh_TW": "從小到大"
+ }
+ },
+ {
+ "ID": "OrderDescending",
+ "Translations": {
+ "ar_SA": "تنازلي",
+ "de_DE": "Absteigend",
+ "el_GR": "Φθίνουσα",
+ "en_US": "Descending",
+ "es_ES": "Descendente",
+ "fr_FR": "Décroissant",
+ "he_IL": "סדר יורד",
+ "it_IT": "Decrescente",
+ "ja_JP": "降順",
+ "ko_KR": "내림차순",
+ "no_NO": "Synkende",
+ "pl_PL": "Malejąco",
+ "pt_BR": "Descendente",
+ "ru_RU": "По Убыванию",
+ "sv_SE": "Fallande",
+ "th_TH": "จากมากไปน้อย",
+ "tr_TR": "Azalan",
+ "uk_UA": "За спаданням",
+ "zh_CN": "降序",
+ "zh_TW": "從大到小"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Dialog_Amiibo.json b/assets/Locales/Dialog_Amiibo.json
new file mode 100644
index 000000000..315025431
--- /dev/null
+++ b/assets/Locales/Dialog_Amiibo.json
@@ -0,0 +1,179 @@
+{
+ "Locales": [
+ {
+ "ID": "ScanAmiiboFromBinFilePickerTitle",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Wähle eine BIN-Datei aus, um ein Amiibo zu scannen.",
+ "el_GR": "",
+ "en_US": "Select a BIN file to scan an Amiibo from",
+ "es_ES": "Selecciona un archivo BIN para escanear un Amiibo",
+ "fr_FR": "Sélectionnez un fichier BIN pour scanner un Amiibo",
+ "he_IL": "",
+ "it_IT": "Seleziona un file BIN da cui scansionare un Amiibo",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "Selecione um arquivo BIN para escanear um Amiibo",
+ "ru_RU": "Выберите BIN-файл для сканирования Amiibo",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Виберіть BIN-файл для сканування Amiibo",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "APITitle",
+ "Translations": {
+ "ar_SA": "أميبو API",
+ "de_DE": "Amiibo-API",
+ "el_GR": "API για Amiibo.",
+ "en_US": "Amiibo API",
+ "es_ES": "API Amiibo",
+ "fr_FR": "API Amiibo",
+ "he_IL": "ממשק תכנות אמיבו",
+ "it_IT": "API Amiibo",
+ "ja_JP": "",
+ "ko_KR": null,
+ "no_NO": "",
+ "pl_PL": "API Amiibo",
+ "pt_BR": "API Amiibo",
+ "ru_RU": "API Amiibo",
+ "sv_SE": "Amiibo-API",
+ "th_TH": "อามิโบ้ API",
+ "tr_TR": "",
+ "uk_UA": "API Amiibo",
+ "zh_CN": null,
+ "zh_TW": null
+ }
+ },
+ {
+ "ID": "APIFailFetchMessage",
+ "Translations": {
+ "ar_SA": "حدث خطأ أثناء جلب المعلومات من API.",
+ "de_DE": "Beim Abrufen von Informationen aus der API ist ein Fehler aufgetreten.",
+ "el_GR": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση πληροφοριών από το API.",
+ "en_US": "An error occured while fetching information from the API.",
+ "es_ES": "Ocurrió un error al recibir información de la API.",
+ "fr_FR": "Une erreur est survenue lors de la récupération des informations de l'API.",
+ "he_IL": "אירעה שגיאה בעת שליפת מידע מהממשק.",
+ "it_IT": "Si è verificato un errore durante il recupero delle informazioni dall'API.",
+ "ja_JP": "API からの情報取得中にエラーが発生しました.",
+ "ko_KR": "API에서 정보를 가져오는 중에 오류가 발생했습니다.",
+ "no_NO": "En feil oppstod under henting av informasjon fra API.",
+ "pl_PL": "Wystąpił błąd podczas pobierania informacji z API.",
+ "pt_BR": "Um erro ocorreu ao tentar obter informações da API.",
+ "ru_RU": "Произошла ошибка при получении информации из API.",
+ "sv_SE": "Ett fel inträffade vid hämtning av information från API.",
+ "th_TH": "เกิดข้อผิดพลาดขณะเรียกข้อมูลจาก API",
+ "tr_TR": "API'dan bilgi alırken bir hata oluştu.",
+ "uk_UA": "Під час отримання інформації з API сталася помилка.",
+ "zh_CN": "从 API 获取信息时出错。",
+ "zh_TW": "從 API 取得資訊時出現錯誤。"
+ }
+ },
+ {
+ "ID": "APIConnectErrorMessage",
+ "Translations": {
+ "ar_SA": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.",
+ "de_DE": "Verbindung zum Amiibo API Server kann nicht hergestellt werden. Der Dienst ist möglicherweise nicht verfügbar oder es existiert keine Internetverbindung.",
+ "el_GR": "Δεν είναι δυνατή η σύνδεση με τον διακομιστή Amiibo API. Η υπηρεσία μπορεί να είναι εκτός λειτουργίας ή μπορεί να χρειαστεί να επαληθεύσετε ότι έχετε ενεργή σύνδεσή στο Διαδίκτυο.",
+ "en_US": "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.",
+ "es_ES": "No se pudo conectar al servidor de la API Amiibo. El servicio puede estar caído o tu conexión a internet puede haberse desconectado.",
+ "fr_FR": "Impossible de se connecter au serveur API Amiibo. Le service est peut-être hors service ou vous devriez peut-être vérifier que votre connexion internet est connectée.",
+ "he_IL": "לא ניתן להתחבר לממשק שרת האמיבו. ייתכן שהשירות מושבת או שתצטרך לוודא שהחיבור לאינטרנט שלך מקוון.",
+ "it_IT": "Impossibile connettersi al server dell'API Amiibo. Il servizio potrebbe non essere disponibile o potresti non essere connesso a Internet.",
+ "ja_JP": "Amiibo API サーバに接続できませんでした. サーバがダウンしているか, インターネット接続に問題があるかもしれません.",
+ "ko_KR": "Amiibo API 서버에 연결할 수 없습니다. 서비스가 다운되었거나 인터넷 연결이 온라인 상태인지 확인이 필요합니다.",
+ "no_NO": "Kan ikke koble til Amiibo API server. Tjenesten kan være nede, eller du må kanskje verifisere at din internettforbindelse er tilkoblet.",
+ "pl_PL": "Nie można połączyć się z serwerem API Amiibo. Usługa może nie działać lub może być konieczne sprawdzenie, czy połączenie internetowe jest online.",
+ "pt_BR": "Não foi possível conectar ao servidor da API Amiibo. O serviço pode estar fora do ar ou você precisa verificar sua conexão com a Internet.",
+ "ru_RU": "Не удалось подключиться к серверу Amiibo API. Служба может быть недоступна или вам может потребоваться проверить ваше интернет-соединение.",
+ "sv_SE": "Kunde inte ansluta till Amiibo API-server. Tjänsten kanske är nere eller så behöver du kontrollera att din internetanslutning fungerar.",
+ "th_TH": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Amiibo API บางบริการอาจหยุดทำงาน หรือไม่คุณต้องทำการตรวจสอบว่าอินเทอร์เน็ตของคุณอยู่ในสถานะเชื่อมต่ออยู่หรือไม่",
+ "tr_TR": "Amiibo API sunucusuna bağlanılamadı. Sunucu çevrimdışı olabilir veya uygun bir internet bağlantınızın olduğunu kontrol etmeniz gerekebilir.",
+ "uk_UA": "Неможливо підключитися до сервера Amiibo API. Можливо, служба не працює або вам потрібно перевірити, чи є підключення до Інтернету.",
+ "zh_CN": "无法连接到 Amiibo API 服务器,服务可能已关闭,或者没有连接互联网。",
+ "zh_TW": "無法連接 Amiibo API 伺服器。服務可能已停機,或者您可能需要確認網際網路連線是否在線上。"
+ }
+ },
+ {
+ "ID": "RenameAmiiboTitle",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Amiibo umbenennen",
+ "el_GR": "",
+ "en_US": "Rename Amiibo",
+ "es_ES": "Renombrar Amiibo",
+ "fr_FR": "Renommer l'Amiibo",
+ "he_IL": "",
+ "it_IT": "Rinomina Amiibo",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "Renomear Amiibo",
+ "ru_RU": "Переименовать Amiibo",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Перейменувати Amiibo",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ScanAmiiboTitle",
+ "Translations": {
+ "ar_SA": "فحص Amiibo",
+ "de_DE": "Amiibo einscannen",
+ "el_GR": "Σαρώστε το Amiibo",
+ "en_US": "Scan Amiibo",
+ "es_ES": "Escanear Amiibo",
+ "fr_FR": "Scanner l'Amiibo",
+ "he_IL": "סרוק את ה-Amiibo",
+ "it_IT": "Scansiona l'Amiibo",
+ "ja_JP": "Amiiboをスキャン",
+ "ko_KR": "Amiibo 스캔하기",
+ "no_NO": "Skanne Amiibo",
+ "pl_PL": "Zeskanuj Amiibo",
+ "pt_BR": "Escanear Amiibo",
+ "ru_RU": "Сканировать Amiibo",
+ "sv_SE": "Skanna Amiibo",
+ "th_TH": "สแกน Amiibo",
+ "tr_TR": "Amiibo'yu tara",
+ "uk_UA": "Сканувати Amiibo",
+ "zh_CN": "扫描 Amiibo",
+ "zh_TW": "掃描 Amiibo"
+ }
+ },
+ {
+ "ID": "ScanAmiiboMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Bitte scanne jetzt dein Amiibo.",
+ "el_GR": "",
+ "en_US": "Please scan your Amiibo now.",
+ "es_ES": "Escanea tu Amiibo ahora.",
+ "fr_FR": "Veuillez scannez votre Amiibo.",
+ "he_IL": "",
+ "it_IT": "Scansiona ora il tuo Amiibo.",
+ "ja_JP": "",
+ "ko_KR": "지금 Amiibo를 스캔하세요.",
+ "no_NO": "Vennligst skann Amiiboene dine nå.",
+ "pl_PL": "",
+ "pt_BR": "Por favor, escaneie seu Amiibo agora.",
+ "ru_RU": "Пожалуйста, сканируйте ваш Amiibo.",
+ "sv_SE": "Skanna din Amiibo nu.",
+ "th_TH": "กรุณาสแกน Amiibo ของคุณตอนนี้",
+ "tr_TR": "",
+ "uk_UA": "Будь ласка, проскануйте Ваш Amiibo.",
+ "zh_CN": "请现在扫描你的 Amiibo",
+ "zh_TW": "請掃描你的 Amiibo。"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/assets/Locales/Dialog_ContentLoading.json b/assets/Locales/Dialog_ContentLoading.json
index 0448b79df..a5f4f023e 100644
--- a/assets/Locales/Dialog_ContentLoading.json
+++ b/assets/Locales/Dialog_ContentLoading.json
@@ -4,7 +4,7 @@
"ID": "UpdatesAddedMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Updates hinzugefügt: {0}",
"el_GR": "",
"en_US": "Updates Added: {0}",
"es_ES": "Actualizaciones Añadidas: {0}",
@@ -29,7 +29,7 @@
"ID": "UpdatesRemovedMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Updates entfernt: {0}",
"el_GR": "",
"en_US": "Updates Removed: {0}",
"es_ES": "Actualizaciones Eliminadas: {0}",
@@ -54,7 +54,7 @@
"ID": "DLCAddedMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "DLC hinzugefügt: {0}",
"el_GR": "",
"en_US": "DLC Added: {0}",
"es_ES": "DLC Añadidos: {0}",
@@ -79,7 +79,7 @@
"ID": "DLCRemovedMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "DLC entfernt: {0}",
"el_GR": "",
"en_US": "DLC Removed: {0}",
"es_ES": "DLC Eliminados: {0}",
diff --git a/assets/Locales/Dialog_FileMenu.json b/assets/Locales/Dialog_FileMenu.json
index 3c0b345a3..9ede52f3c 100644
--- a/assets/Locales/Dialog_FileMenu.json
+++ b/assets/Locales/Dialog_FileMenu.json
@@ -4,7 +4,7 @@
"ID": "LoadApplicationFromFileFilePickerTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wähle eine Switch-Anwendungsdatei zum Laden aus",
"el_GR": "",
"en_US": "Select a Switch application file to load",
"es_ES": "Selecciona un archivo de aplicación de Switch para cargar",
@@ -29,7 +29,7 @@
"ID": "LoadUnpackedApplicationFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wähle einen Ordner mit einer entpackten Switch-Anwendung zum Laden",
"el_GR": "",
"en_US": "Select a folder containing an unpacked Switch application to load",
"es_ES": "Selecciona una carpeta que contenga una aplicación de Switch descomprimida para cargar",
@@ -54,7 +54,7 @@
"ID": "LoadUpdatesAndDLCFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wähle einen oder mehrere Ordner zum Laden von Updates und DLC aus",
"el_GR": "",
"en_US": "Select one or more folders to bulk load updates and DLC from",
"es_ES": "Selecciona una o más carpetas para cargar de forma masiva actualizaciones y DLC",
diff --git a/assets/Locales/Dialog_Skylanders.json b/assets/Locales/Dialog_Skylanders.json
new file mode 100644
index 000000000..8aa8346bc
--- /dev/null
+++ b/assets/Locales/Dialog_Skylanders.json
@@ -0,0 +1,29 @@
+{
+ "Locales": [
+ {
+ "ID": "ScanSkylanderFilePickerTitle",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Wähle eine BIN-, SKY-, DMP- oder DUMP-Datei aus, um einen Skylander zu scannen.",
+ "el_GR": "",
+ "en_US": "Select a BIN, SKY, DMP, or DUMP file to scan a Skylander from",
+ "es_ES": "Selecciona un archivo BIN, SKY, DMP o DUMP para escanear un Skylander",
+ "fr_FR": "Sélectionnez un fichier BIN, SKY, DMP ou DUMP pour scanner un Skylander",
+ "he_IL": "",
+ "it_IT": "Seleziona un file BIN, SKY, DMP o DUMP da cui scansionare uno Skylander",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "Selecione um arquivo BIN, SKY, DMP ou DUMP para escanear um Skylander",
+ "ru_RU": "Выберите файл BIN, SKY, DMP или DUMP для сканирования Skylander",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Виберіть файл BIN, SKY, DMP або DUMP для сканування Skylander",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Dialog_XCITrimmer.json b/assets/Locales/Dialog_XCITrimmer.json
new file mode 100644
index 000000000..05497935c
--- /dev/null
+++ b/assets/Locales/Dialog_XCITrimmer.json
@@ -0,0 +1,304 @@
+{
+ "Locales": [
+ {
+ "ID": "PrimaryMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Entfernt ungenutzten Speicherplatz aus der XCI-Datei, um ihre Dateigröße zu verringern.",
+ "el_GR": "",
+ "en_US": "Removes unused space from the XCI to reduce its file size.",
+ "es_ES": "Elimina el espacio no utilizado del XCI para reducir su tamaño.",
+ "fr_FR": "Supprime l’espace inutilisé du XCI afin de réduire sa taille.",
+ "he_IL": "",
+ "it_IT": "Rimuove lo spazio inutilizzato dall'XCI per ridurne le dimensioni.",
+ "ja_JP": "",
+ "ko_KR": "XCI에서 사용되지 않는 공간을 제거하여 파일 크기를 줄입니다.",
+ "no_NO": "Fjerner ubrukt plass fra XCI-filen for å redusere filstørrelsen.",
+ "pl_PL": "Usuwa nieużywane miejsce z pliku XCI, aby zmniejszyć jego rozmiar.",
+ "pt_BR": "Remove o espaço não utilizado do XCI para reduzir seu tamanho.",
+ "ru_RU": "Удаляет неиспользуемое пространство из XCI, уменьшая размер файла.",
+ "sv_SE": "Tar bort oanvänt utrymme från XCI-filen för att minska filstorleken.",
+ "th_TH": "ลบพื้นที่ที่ไม่ได้ใช้งานออกจาก XCI เพื่อลดขนาดไฟล์",
+ "tr_TR": "",
+ "uk_UA": "Видаляє невикористаний простір із XCI, зменшуючи розмір файлу.",
+ "zh_CN": "移除 XCI 中未使用的空间以减小文件大小。",
+ "zh_TW": "移除 XCI 中未使用的空間以減少檔案大小。"
+ }
+ },
+ {
+ "ID": "SecondaryMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Datei: {0:n} MB • Spiel: {1:n} MB\n\nErsparnis: {2:n} MB",
+ "el_GR": "",
+ "en_US": "File: {0:n} MB • Game: {1:n} MB\n\nSavings: {2:n} MB",
+ "es_ES": "Archivo: {0:n} MB • Juego: {1:n} MB\n\nAhorro: {2:n} MB",
+ "fr_FR": "Fichier: {0:n} Mo • Jeu: {1:n} Mo\n\nÉconomies: {2:n} Mo",
+ "he_IL": "",
+ "it_IT": "File: {0:n} MB • Gioco: {1:n} MB\n\nRisparmio: {2:n} MB",
+ "ja_JP": "",
+ "ko_KR": "파일: {0:n} MB • 게임: {1:n} MB\n\n절약: {2:n} MB",
+ "no_NO": "Fil: {0:n} MB • Spill: {1:n} MB\n\nBesparelse: {2:n} MB",
+ "pl_PL": "Plik: {0:n} MB • Gra: {1:n} MB\n\nOszczędności: {2:n} MB",
+ "pt_BR": "Arquivo: {0:n} MB • Jogo: {1:n} MB\n\nEconomia: {2:n} MB",
+ "ru_RU": "Файл: {0:n} МБ • Игра: {1:n} МБ\n\nЭкономия: {2:n} МБ",
+ "sv_SE": "Fil: {0:n} MB • Spel: {1:n} MB\n\nBesparing: {2:n} MB",
+ "th_TH": "ไฟล์: {0:n} MB • เกม: {1:n} MB\n\nการประหยัด: {2:n} MB",
+ "tr_TR": "",
+ "uk_UA": "Файл: {0:n} МБ • Гра: {1:n} МБ\n\nЕкономія: {2:n} МБ",
+ "zh_CN": "文件: {0:n} MB • 游戏: {1:n} MB\n\n节省: {2:n} MB",
+ "zh_TW": "檔案: {0:n} MB • 遊戲: {1:n} MB\n\n節省: {2:n} MB"
+ }
+ },
+ {
+ "ID": "NoTrimNecessaryMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI muss nicht getrimmt werden. Prüfen Sie die Logs für Details.",
+ "el_GR": "",
+ "en_US": "XCI does not require trimming. Check logs for details.",
+ "es_ES": "El XCI no necesita ser recortado. Verifica los logs para detalles.",
+ "fr_FR": "Le XCI n’a pas besoin d’être réduit. Référez-vous aux journaux pour détails.",
+ "he_IL": "",
+ "it_IT": "Non è necessario ridurre la dimensione del XCI. Controlla i log per dettagli.",
+ "ja_JP": "",
+ "ko_KR": "XCI는 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인.",
+ "no_NO": "XCI trenger ikke å trimmes. Sjekk loggene for detaljer.",
+ "pl_PL": "XCI nie wymaga przycinania. Sprawdź dzienniki, aby uzyskać szczegóły.",
+ "pt_BR": "O XCI não precisa ser reduzido. Verifique os logs para detalhes.",
+ "ru_RU": "XCI не требует обрезки. Проверьте логи для подробностей.",
+ "sv_SE": "XCI behöver inte optimeras. Kontrollera loggen för detaljer.",
+ "th_TH": "XCI ไม่จำเป็นต้องถูกตัดแต่ง โปรดตรวจสอบบันทึกสำหรับรายละเอียด",
+ "tr_TR": "",
+ "uk_UA": "XCI не потребує обрізання. Перевірте журнали для отримання деталей.",
+ "zh_CN": "XCI 不需要被瘦身。查看日志以获得更多细节。",
+ "zh_TW": "XCI 不需要修剪。檢查日誌以取得更多資訊。"
+ }
+ },
+ {
+ "ID": "NoUntrimPossibleMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Das Trimmen der XCI kann nicht rückgängig gemacht werden. Prüfen Sie die Logs für Details.",
+ "el_GR": "",
+ "en_US": "XCI cannot be untrimmed. Check logs for details.",
+ "es_ES": "El recorte del XCI no puede ser deshecho. Verifica los registros para detalles.",
+ "fr_FR": "Le XCI ne peut être restauré. Référez-vous aux journaux pour détails.",
+ "he_IL": "",
+ "it_IT": "XCI non può essere ripristinato. Controlla i log per dettagli.",
+ "ja_JP": "",
+ "ko_KR": "XCI는 복원할 수 없습니다. 자세한 내용은 로그를 확인.",
+ "no_NO": "XCI kan ikke gjenopprettes. Sjekk loggene for detaljer.",
+ "pl_PL": "XCI nie może zostać przywrócone. Sprawdź dzienniki, aby uzyskać szczegóły.",
+ "pt_BR": "XCI não pode ser desfeito. Verifique os logs para detalhes.",
+ "ru_RU": "XCI не может быть восстановлен. Проверьте журналы для подробностей.",
+ "sv_SE": "XCI kan inte återställas. Kontrollera loggen för detaljer.",
+ "th_TH": "ไม่สามารถคืนค่า XCI ได้ โปรดตรวจสอบบันทึกสำหรับรายละเอียด",
+ "tr_TR": "",
+ "uk_UA": "XCI не можна відновити. Перевірте журнали для деталей.",
+ "zh_CN": "XCI 不能恢复。查看日志以获取详情。",
+ "zh_TW": "XCI 無法恢復。檢查日誌以取得詳情。"
+ }
+ },
+ {
+ "ID": "ReadOnlyFileCannotFixMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI ist schreibgeschützt und konnte nicht schreibbar gemacht werden. Prüfen Sie die Logs für Details.",
+ "el_GR": "",
+ "en_US": "XCI is read-only and could not be made writable. Check logs for details.",
+ "es_ES": "XCI es solo lectura y no se puede escribir. Verifica los registros para detalles.",
+ "fr_FR": "XCI en lecture seule et n'a pas pu être rendu écrivable. Référez-vous aux journaux pour détails.",
+ "he_IL": "",
+ "it_IT": "XCI è solo lettura e non può essere scritto. Controlla i log per dettagli.",
+ "ja_JP": "",
+ "ko_KR": "XCI 파일은 읽기 전용이며 쓰기 불가. 로그를 확인하십시오.",
+ "no_NO": "XCI er skrivebeskyttet og kunne ikke gjøres skrivbar. Sjekk loggene for detaljer.",
+ "pl_PL": "XCI jest tylko do odczytu i nie można zapisać. Sprawdź logi dla szczegółów.",
+ "pt_BR": "XCI é somente leitura e não pode ser gravado. Verifique os logs para detalhes.",
+ "ru_RU": "XCI только для чтения и не стал доступен для записи. Проверьте журналы для подробностей.",
+ "sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information",
+ "th_TH": "XCI เป็นอ่านอย่างเดียวและไม่สามารถเขียนได้ ตรวจสอบบันทึกสำหรับรายละเอียด",
+ "tr_TR": "",
+ "uk_UA": "XCI тільки для читання і не можна записати. Перевірте логи для деталей.",
+ "zh_CN": "XCI 只读,无法写入。查看日志以获取详情。",
+ "zh_TW": "XCI 檔案唯讀,無法寫入。檢查日誌以取得詳情。"
+ }
+ },
+ {
+ "ID": "SizeChangedMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI-Größe hat sich seit dem letzten Scan geändert. Stellen Sie sicher, dass die Datei nicht beschrieben wird, und versuchen Sie es erneut.",
+ "el_GR": "",
+ "en_US": "XCI size changed since last scan. Ensure the file is not being written to and try again.",
+ "es_ES": "El tamaño de XCI ha cambiado desde que fue escaneado. Verifica que no se esté escribiendo al archivo y vuelve a intentarlo.",
+ "fr_FR": "La taille de XCI a changé depuis son analyse. Vérifiez que le fichier n’est pas en cours d’écriture, puis réessayez.",
+ "he_IL": "",
+ "it_IT": "La dimensione di XCI è cambiata da quando è stato scansionato. Controlla che il file non sia scritto e riprova.",
+ "ja_JP": "",
+ "ko_KR": "XCI 크기가 스캔 후 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.",
+ "no_NO": "XCI har endret størrelse siden den ble skannet. Kontroller at det ikke skrives til filen, og prøv på nytt.",
+ "pl_PL": "Rozmiar XCI zmienił się od momentu zeskanowania. Sprawdź, czy plik nie jest zapisywany, a następnie spróbuj ponownie.",
+ "pt_BR": "O tamanho de XCI mudou desde que foi escaneado. Verifique se o arquivo não está sendo gravado e tente novamente.",
+ "ru_RU": "Размер XCI изменился после сканирования. Проверьте, не записывается ли файл, и попробуйте снова.",
+ "sv_SE": "XCI har ändrats i storlek sedan den lästes av. Kontrollera att filen inte skrivs till och försök igen.",
+ "th_TH": "ขนาด XCI เปลี่ยนไปตั้งแต่การสแกนครั้งล่าสุด ตรวจสอบว่าไฟล์ไม่ได้ถูกเขียน และลองใหม่",
+ "tr_TR": "",
+ "uk_UA": "Розмір XCI змінився з моменту сканування. Перевірте, чи не записується файл, та спробуйте знову.",
+ "zh_CN": "XCI 在扫描后大小发生了变化。请检查文件是否未被写入,然后重试。",
+ "zh_TW": "XCI 檔案大小自上次掃描以來已經改變。請檢查檔案是否未被寫入,然後再嘗試。"
+ }
+ },
+ {
+ "ID": "FreeSpaceCheckFailedMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI enthält Daten im freien Speicherbereich. Es ist unsicher zu trimmen.",
+ "el_GR": "",
+ "en_US": "XCI has data in the free space area. It is not safe to trim.",
+ "es_ES": "XCI tiene datos en el área de espacio libre. No es seguro recortarlo.",
+ "fr_FR": "XCI contient des données dans la zone d'espace libre. Il n'est pas sûr de le réduire.",
+ "he_IL": "",
+ "it_IT": "XCI contiene dati nell'area di spazio libero. Non è sicuro ridurre la sua dimensione.",
+ "ja_JP": "",
+ "ko_KR": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않습니다.",
+ "no_NO": "XCI har data i den ledige plassen. Det er ikke trygt å trimme den.",
+ "pl_PL": "XCI zawiera dane w obszarze wolnego miejsca. Nie jest bezpieczne go przycinać.",
+ "pt_BR": "XCI tem dados na área de espaço livre. Não é seguro reduzi-lo.",
+ "ru_RU": "XCI содержит данные в свободной области. Его обрезка небезопасна.",
+ "sv_SE": "XCI har data i det lediga utrymmet. Det är inte säkert att optimera.",
+ "th_TH": "XCI มีข้อมูลในพื้นที่ว่าง จึงไม่ปลอดภัยที่จะทำการตัดแต่ง",
+ "tr_TR": "",
+ "uk_UA": "XCI містить дані в зоні вільного простору. Тому обрізка небезпечна.",
+ "zh_CN": "XCI 文件的空闲区域内有数据。不能安全瘦身。",
+ "zh_TW": "XCI 檔案有數據儲存於空閒區域。修剪不安全。"
+ }
+ },
+ {
+ "ID": "InvalidDataMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI enthält ungültige Daten. Prüfen Sie die Logs für Details.",
+ "el_GR": "",
+ "en_US": "XCI contains invalid data. Check logs for details.",
+ "es_ES": "XCI contiene datos inválidos. Lee el registro para detalles.",
+ "fr_FR": "XCI contient des données invalides. Référez-vous aux journaux pour détails.",
+ "he_IL": "",
+ "it_IT": "XCI contiene dati non validi. Controlla i log per dettagli.",
+ "ja_JP": "",
+ "ko_KR": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 로그를 확인하세요.",
+ "no_NO": "XCI-filen inneholder ugyldige data. Sjekk loggene for detaljer.",
+ "pl_PL": "XCI zawiera nieprawidłowe dane. Sprawdź dzienniki, aby uzyskać szczegóły.",
+ "pt_BR": "XCI contém dados inválidos. Verifique os logs para detalhes.",
+ "ru_RU": "XCI содержит недопустимые данные. Проверьте журналы для подробностей.",
+ "sv_SE": "XCI-filen innehåller ogiltig data. Kontrollera loggen för detaljer.",
+ "th_TH": "XCI มีข้อมูลที่ไม่ถูกต้อง โปรดตรวจสอบบันทึกสำหรับรายละเอียด",
+ "tr_TR": "",
+ "uk_UA": "XCI містить недійсні дані. Перевірте журнали для деталей.",
+ "zh_CN": "XCI 文件含有无效数据。查看日志以获得更多细节。",
+ "zh_TW": "XCI 檔案帶有無效的數據。檢查日誌以取得更多資訊"
+ }
+ },
+ {
+ "ID": "WriteErrorMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI konnte nicht zum Schreiben geöffnet werden. Prüfen Sie die Logs für Details.",
+ "el_GR": "",
+ "en_US": "XCI could not be opened for writing. Check logs for details.",
+ "es_ES": "XCI no se puede abrir para escribir. Lee el registro para detalles.",
+ "fr_FR": "XCI n'a pas pu être ouvert pour écriture. Consultez les journaux pour détails.",
+ "he_IL": "",
+ "it_IT": "XCI non può essere aperto in scrittura. Controlla i log per dettagli.",
+ "ja_JP": "",
+ "ko_KR": "XCI를 쓰기 위해 열 수 없습니다. 로그를 확인하세요.",
+ "no_NO": "XCI kunne ikke åpnes for skriving. Sjekk loggene for detaljer.",
+ "pl_PL": "Nie można otworzyć XCI do zapisu. Sprawdź dzienniki, aby uzyskać szczegóły.",
+ "pt_BR": "XCI não pôde ser aberto para gravação. Verifique os logs para detalhes.",
+ "ru_RU": "Не удалось открыть XCI для записи. Проверьте журналы для подробностей.",
+ "sv_SE": "XCI kunde inte öppnas för skrivning. Kontrollera loggen för detaljer.",
+ "th_TH": "ไม่สามารถเปิด XCI เพื่อเขียนข้อมูลได้ โปรดตรวจสอบบันทึกสำหรับรายละเอียด",
+ "tr_TR": "",
+ "uk_UA": "Не вдалося відкрити XCI для запису. Перевірте журнали для деталей.",
+ "zh_CN": "XCI 不能写入。查看日志以获得更多细节。",
+ "zh_TW": "XCI 無法開啟以進行寫入。請檢查日誌以取得更多資訊。"
+ }
+ },
+ {
+ "ID": "TrimFailedMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmen der XCI fehlgeschlagen.",
+ "el_GR": "",
+ "en_US": "Failed to trim XCI.",
+ "es_ES": "El recorte del XCI falló.",
+ "fr_FR": "La réduction du XCI a échoué.",
+ "he_IL": "",
+ "it_IT": "Riduzione del XCI fallita.",
+ "ja_JP": "",
+ "ko_KR": "XCI 트리밍에 실패했습니다.",
+ "no_NO": "Trimming av XCI mislyktes.",
+ "pl_PL": "Nie udało się przyciąć XCI.",
+ "pt_BR": "A redução do XCI falhou.",
+ "ru_RU": "Обрезка XCI не удалась.",
+ "sv_SE": "Optimering av XCI misslyckades.",
+ "th_TH": "การตัดแต่ง XCI ล้มเหลว",
+ "tr_TR": "",
+ "uk_UA": "Не вдалося обрізати XCI.",
+ "zh_CN": "XCI 瘦身失败。",
+ "zh_TW": "修剪 XCI 失敗。"
+ }
+ },
+ {
+ "ID": "TrimCancelledMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Der Vorgang wurde abgebrochen.",
+ "el_GR": "",
+ "en_US": "The operation was cancelled.",
+ "es_ES": "La operación fue cancelada.",
+ "fr_FR": "L'opération a été annulée.",
+ "he_IL": "",
+ "it_IT": "L'operazione è stata annullata.",
+ "ja_JP": "",
+ "ko_KR": "작업이 취소되었습니다.",
+ "no_NO": "Operasjonen ble avbrutt.",
+ "pl_PL": "",
+ "pt_BR": "A operação foi cancelada.",
+ "ru_RU": "Операция была отменена.",
+ "sv_SE": "Åtgärden avbröts.",
+ "th_TH": "การดำเนินการถูกยกเลิกแล้ว.",
+ "tr_TR": "",
+ "uk_UA": "Операцію перервано.",
+ "zh_CN": "操作已取消。",
+ "zh_TW": "操作已取消。"
+ }
+ },
+ {
+ "ID": "NoOperationPerformedMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Es wurde keine Aktion durchgeführt.",
+ "el_GR": "",
+ "en_US": "No operation was performed.",
+ "es_ES": "No se realizó ninguna operación.",
+ "fr_FR": "Aucune opération n'a été effectuée.",
+ "he_IL": "",
+ "it_IT": "Non è stata effettuata alcuna operazione.",
+ "ja_JP": "",
+ "ko_KR": "작업이 수행되지 않았습니다.",
+ "no_NO": "Ingen operasjon ble utført.",
+ "pl_PL": "",
+ "pt_BR": "Nenhuma operação foi realizada.",
+ "ru_RU": "Операция не была выполнена.",
+ "sv_SE": "Ingen åtgärd genomfördes.",
+ "th_TH": "ไม่มีการดำเนินการใด ๆ",
+ "tr_TR": "",
+ "uk_UA": "Операцію не було виконано.",
+ "zh_CN": "未执行任何操作。",
+ "zh_TW": "沒有執行任何操作。"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/Error.json b/assets/Locales/Error.json
index 637bc0584..623028207 100644
--- a/assets/Locales/Error.json
+++ b/assets/Locales/Error.json
@@ -29,7 +29,7 @@
"ID": "NoUnpackedApplicationFoundInFolder",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Bitte wähle einen entpackten Anwendungsordner mit gültigem ExeFS oder NSO/NRO aus.",
"el_GR": "",
"en_US": "Please select an unpacked application folder with a valid ExeFS or NSO/NRO.",
"es_ES": "",
diff --git a/assets/Locales/GameListContextMenu.json b/assets/Locales/GameListContextMenu.json
new file mode 100644
index 000000000..52fee5164
--- /dev/null
+++ b/assets/Locales/GameListContextMenu.json
@@ -0,0 +1,29 @@
+{
+ "Locales": [
+ {
+ "ID": "TrimXCIButton",
+ "Translations": {
+ "ar_SA": "تقليم XCI",
+ "de_DE": "Zuschneiden der XCI",
+ "el_GR": "Κοπή XCI",
+ "en_US": "Trim XCI",
+ "es_ES": "Recortar XCI",
+ "fr_FR": "Réduire le XCI",
+ "he_IL": "חתוך XCI",
+ "it_IT": "Riduci il XCI",
+ "ja_JP": "XCIをトリム",
+ "ko_KR": "XCI 트림",
+ "no_NO": "Trim XCI-filen",
+ "pl_PL": "Przytnij XCI",
+ "pt_BR": "Reduzir o XCI",
+ "ru_RU": "Обрезать XCI",
+ "sv_SE": "Optimera XCI",
+ "th_TH": "ลดขนาด XCI",
+ "tr_TR": "XCI'yi Kırp",
+ "uk_UA": "Нарізка XCI",
+ "zh_CN": "精简 XCI",
+ "zh_TW": "修剪 XCI"
+ }
+ }
+ ]
+}
diff --git a/assets/Locales/MenuBar_Actions.json b/assets/Locales/MenuBar_Actions.json
index e7f094959..2508de727 100644
--- a/assets/Locales/MenuBar_Actions.json
+++ b/assets/Locales/MenuBar_Actions.json
@@ -229,25 +229,25 @@
"ID": "XCITrimmerButton",
"Translations": {
"ar_SA": "",
- "de_DE": "XCI-Dateien trimmen",
+ "de_DE": "XCI-Trimmer",
"el_GR": "",
- "en_US": "Trim XCI Files",
- "es_ES": "Recortar Archivos XCI",
- "fr_FR": "Réduire les Fichiers XCI",
+ "en_US": "XCI Trimmer",
+ "es_ES": "Recortador de XCI",
+ "fr_FR": "Réducteur de XCI",
"he_IL": "",
- "it_IT": "Riduci dimensioni dei file XCI",
+ "it_IT": "Trimmer XCI",
"ja_JP": "",
"ko_KR": "XCI 파일 트리머",
"no_NO": "Trim XCI-filer",
- "pl_PL": "",
- "pt_BR": "Reduzir Arquivos XCI",
- "ru_RU": "Обрезать XCI файлы",
- "sv_SE": "Optimera XCI-filer",
- "th_TH": "ตัดแต่งไฟล์ XCI",
- "tr_TR": "",
- "uk_UA": "Обрізати XCI файли",
- "zh_CN": "瘦身 XCI 文件",
- "zh_TW": "修剪 XCI 檔案"
+ "pl_PL": "Przycinacz XCI",
+ "pt_BR": "Cortador de XCI",
+ "ru_RU": "Триммер XCI",
+ "sv_SE": "XCI-trimmer",
+ "th_TH": "",
+ "tr_TR": "XCI Kesici",
+ "uk_UA": "Тример XCI",
+ "zh_CN": "",
+ "zh_TW": ""
}
},
{
@@ -378,26 +378,26 @@
{
"ID": "ScanAmiiboFromBinButton",
"Translations": {
- "ar_SA": "مسح Amiibo (.BIN)",
- "de_DE": "Amiibo scannen (.BIN)",
- "el_GR": "Σάρωση Amiibo (.BIN)",
- "en_US": "Scan Amiibo (.BIN)",
- "es_ES": "Escanear un Amiibo (.BIN)",
- "fr_FR": "Scanner un Amiibo (.BIN)",
- "he_IL": "סרוק Amiibo (.BIN)",
- "it_IT": "Scansiona un Amiibo (.BIN)",
- "ja_JP": "Amiibo をスキャン (.BIN)",
- "ko_KR": "Amiibo 스캔 (.BIN)",
- "no_NO": "Skann en Amiibo (.BIN)",
- "pl_PL": "Skanuj Amiibo (.BIN)",
- "pt_BR": "Escaneie um Amiibo (.BIN)",
- "ru_RU": "Сканировать Amiibo (.BIN)",
- "sv_SE": "Skanna en Amiibo (.BIN)",
- "th_TH": "สแกนอามีโบ (.BIN)",
- "tr_TR": "Amiibo Tara (.BIN)",
- "uk_UA": "Сканувати Amiibo (.BIN)",
- "zh_CN": "扫描 Amiibo (.BIN)",
- "zh_TW": "掃瞄 Amiibo (.BIN)"
+ "ar_SA": "مسح Amiibo (BIN)",
+ "de_DE": "Amiibo scannen (BIN)",
+ "el_GR": "Σάρωση Amiibo (BIN)",
+ "en_US": "Scan Amiibo (BIN)",
+ "es_ES": "Escanear un Amiibo (BIN)",
+ "fr_FR": "Scanner un Amiibo (BIN)",
+ "he_IL": "סרוק Amiibo (BIN)",
+ "it_IT": "Scansiona un Amiibo (BIN)",
+ "ja_JP": "Amiibo をスキャン (BIN)",
+ "ko_KR": "Amiibo 스캔 (BIN)",
+ "no_NO": "Skann en Amiibo (BIN)",
+ "pl_PL": "Skanuj Amiibo (BIN)",
+ "pt_BR": "Escaneie um Amiibo (BIN)",
+ "ru_RU": "Сканировать Amiibo (BIN)",
+ "sv_SE": "Skanna en Amiibo (BIN)",
+ "th_TH": "สแกนอามีโบ (BIN)",
+ "tr_TR": "Amiibo Tara (BIN)",
+ "uk_UA": "Сканувати Amiibo (BIN)",
+ "zh_CN": "扫描 Amiibo (BIN)",
+ "zh_TW": "掃瞄 Amiibo (BIN)"
}
},
{
@@ -450,6 +450,31 @@
"zh_TW": "移除 Skylander"
}
},
+ {
+ "ID": "SimulateWakeUpMessageButton",
+ "Translations": {
+ "ar_SA": "محاكاة رسالة الاستيقاظ",
+ "de_DE": "Aufwachnachricht simulieren",
+ "el_GR": "Προσομοίωση Μηνύματος Αφύπνισης",
+ "en_US": "Simulate Wake-Up Message",
+ "es_ES": "Simular Mensaje de Reactivación",
+ "fr_FR": "Simuler un Message de Réveil",
+ "he_IL": "דמה הודעת השכמה",
+ "it_IT": "Simula messaggio di risveglio",
+ "ja_JP": "スリープ復帰メッセージをシミュレート",
+ "ko_KR": "절전 모드 해제 메시지 시뮬레이션",
+ "no_NO": "Simuler oppvåknings-melding",
+ "pl_PL": "Symuluj wiadomość wybudzania",
+ "pt_BR": "Simular Mensagem de Acordar o Console",
+ "ru_RU": "Имитировать сообщение пробуждения",
+ "sv_SE": "Simulera uppvakningsmeddelande",
+ "th_TH": "จำลองการปลุกอุปกรณ์ให้ทำงาน",
+ "tr_TR": "Uyandırma Mesajı Simüle Et",
+ "uk_UA": "Симулювати повідомлення про пробудження",
+ "zh_CN": "模拟唤醒消息",
+ "zh_TW": "模擬喚醒訊息"
+ }
+ },
{
"ID": "TakeScreenshotButton",
"Translations": {
diff --git a/assets/Locales/MenuBar_File.json b/assets/Locales/MenuBar_File.json
index c1f538905..3ba7eedbb 100644
--- a/assets/Locales/MenuBar_File.json
+++ b/assets/Locales/MenuBar_File.json
@@ -154,7 +154,7 @@
"ID": "OpenScreenshotsFolderButton",
"Translations": {
"ar_SA": "",
- "de_DE": "Screenshots-Ordner öffnen",
+ "de_DE": "Bildschirmfoto-Ordner öffnen",
"el_GR": "",
"en_US": "Open Screenshots Folder",
"es_ES": "Abrir Carpeta de Capturas de Pantalla",
@@ -179,7 +179,7 @@
"ID": "AssociateFileTypesButton",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Dateitypen zuordnen",
"el_GR": "",
"en_US": "Associate File Types",
"es_ES": "",
@@ -204,7 +204,7 @@
"ID": "RemoveFileTypeAssociationsButton",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Dateityp-Zuordnungen entfernen",
"el_GR": "",
"en_US": "Remove File Type Associations",
"es_ES": "",
@@ -229,7 +229,7 @@
"ID": "FileTypeAssociationsToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Ordnet unterstützte Dateitypen (NSP, XCI, NCA, NRO, NSO) Ryujinx zu, sodass sie beim Doppelklick im Dateimanager automatisch geöffnet werden. Das Entfernen der Dateityp-Zuordnungen deaktiviert dieses Verhalten.",
"el_GR": "",
"en_US": "Associates supported file types (NSP, XCI, NCA, NRO, NSO) with Ryujinx so they open automatically when double-clicked in your file manager. Removing file type associations stops this behavior.",
"es_ES": "",
diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json
index d93a2b5b7..447009aba 100644
--- a/assets/Locales/Root.json
+++ b/assets/Locales/Root.json
@@ -154,11 +154,11 @@
"ID": "SettingsTabSystemGCLowLatency",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Niedriglatenz-Garbage-Collector verwenden",
"el_GR": "",
"en_US": "Use Low-latency Garbage Collector",
"es_ES": "Usa recolección de basura de baja latencia",
- "fr_FR": "",
+ "fr_FR": "Utiliser le Collecteur de Déchets à Faible Latence",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
@@ -175,181 +175,6 @@
"zh_TW": ""
}
},
- {
- "ID": "MenuBarOptions",
- "Translations": {
- "ar_SA": "_خيارات",
- "de_DE": "_Optionen",
- "el_GR": "_Επιλογές",
- "en_US": "_Options",
- "es_ES": "_Opciones",
- "fr_FR": null,
- "he_IL": "_אפשרויות",
- "it_IT": "_Opzioni",
- "ja_JP": "オプション(_O)",
- "ko_KR": "옵션(_O)",
- "no_NO": "_Alternativer",
- "pl_PL": "_Opcje",
- "pt_BR": "_Opções",
- "ru_RU": "_Настройки",
- "sv_SE": "_Inställningar",
- "th_TH": "_ตัวเลือก",
- "tr_TR": "_Seçenekler",
- "uk_UA": "_Параметри",
- "zh_CN": "选项(_O)",
- "zh_TW": "選項(_O)"
- }
- },
- {
- "ID": "MenuBarOptionsToggleFullscreen",
- "Translations": {
- "ar_SA": "التبديل إلى وضع ملء الشاشة",
- "de_DE": "Vollbild",
- "el_GR": "Λειτουργία Πλήρους Οθόνης",
- "en_US": "Toggle Fullscreen",
- "es_ES": "Cambiar a Pantalla Completa.",
- "fr_FR": "Basculer en Plein Écran",
- "he_IL": "שנה מצב- מסך מלא",
- "it_IT": "Schermo intero",
- "ja_JP": "全画面切り替え",
- "ko_KR": "전체 화면 전환",
- "no_NO": "Fullskjermsvisning av/på",
- "pl_PL": "Przełącz na tryb pełnoekranowy",
- "pt_BR": "Mudar para Tela Cheia",
- "ru_RU": "Переключить полноэкранный режим",
- "sv_SE": "Växla helskärm",
- "th_TH": "สลับเป็นโหมดเต็มหน้าจอ",
- "tr_TR": "Tam Ekran Modunu Aç",
- "uk_UA": "На весь екран",
- "zh_CN": "切换全屏",
- "zh_TW": "切換全螢幕模式"
- }
- },
- {
- "ID": "MenuBarOptionsStartGamesInFullscreen",
- "Translations": {
- "ar_SA": "ابدأ الألعاب في وضع ملء الشاشة",
- "de_DE": "Spiele im Vollbildmodus starten",
- "el_GR": "Εκκίνηση Παιχνιδιών σε Πλήρη Οθόνη",
- "en_US": "Start Games in Fullscreen Mode",
- "es_ES": "Iniciar Juegos en Pantalla Completa",
- "fr_FR": "Démarrer les Jeux en Plein Écran",
- "he_IL": "התחל משחקים במסך מלא",
- "it_IT": "Avvia i giochi a schermo intero",
- "ja_JP": "全画面モードでゲームを開始",
- "ko_KR": "전체 화면 모드로 게임 시작",
- "no_NO": "Start spill i Fullskjermmodus",
- "pl_PL": "Uruchamiaj gry w trybie pełnoekranowym",
- "pt_BR": "Iniciar Jogos em Tela Cheia",
- "ru_RU": "Запускать игры в полноэкранном режиме",
- "sv_SE": "Starta spel i helskärmsläge",
- "th_TH": "เริ่มเกมในโหมดเต็มหน้าจอ",
- "tr_TR": "Oyunları Tam Ekran Modunda Başlat",
- "uk_UA": "Запускати ігри на весь екран",
- "zh_CN": "全屏模式启动游戏",
- "zh_TW": "使用全螢幕模式啟動遊戲"
- }
- },
- {
- "ID": "MenuBarOptionsStartGamesWithoutUI",
- "Translations": {
- "ar_SA": "",
- "de_DE": "Spiele ohne Benutzeroberfläche starten",
- "el_GR": "",
- "en_US": "Start Games with UI Hidden",
- "es_ES": "Iniziar Juegos con la Interfaz Oculta",
- "fr_FR": "Démarrer les Jeux avec l’Interface Cachée",
- "he_IL": "",
- "it_IT": "Avvia i giochi con l'interfaccia nascosta",
- "ja_JP": "",
- "ko_KR": "UI를 숨긴 상태에서 게임 시작",
- "no_NO": "Start Spillet med UI Gjemt",
- "pl_PL": "",
- "pt_BR": "Iniciar Jogos Ocultando a Interface",
- "ru_RU": "Запускать игры скрывая интерфейс",
- "sv_SE": "Starta spel med dolt användargränssnitt",
- "th_TH": "เริ่มเกมโดยซ่อน UI",
- "tr_TR": "",
- "uk_UA": "Запускати ігри з прихованим інтерфейсом",
- "zh_CN": "启动游戏时隐藏 UI",
- "zh_TW": "開啟遊戲時隱藏 UI"
- }
- },
- {
- "ID": "MenuBarOptionsSettings",
- "Translations": {
- "ar_SA": "_الإعدادات",
- "de_DE": "_Einstellungen",
- "el_GR": "_Ρυθμίσεις",
- "en_US": "_Settings",
- "es_ES": "_Configuración",
- "fr_FR": "_Paramètres",
- "he_IL": "_הגדרות",
- "it_IT": "_Impostazioni",
- "ja_JP": "設定(_S)",
- "ko_KR": "설정(_S)",
- "no_NO": "_Innstillinger",
- "pl_PL": "_Ustawienia",
- "pt_BR": "_Configurações",
- "ru_RU": "_Параметры",
- "sv_SE": "_Inställningar",
- "th_TH": "_ตั้งค่า",
- "tr_TR": "_Seçenekler",
- "uk_UA": "_Налаштування",
- "zh_CN": "设置(_S)",
- "zh_TW": "設定(_S)"
- }
- },
- {
- "ID": "MenuBarOptionsManageUserProfiles",
- "Translations": {
- "ar_SA": "_ملفات المستخدمين",
- "de_DE": "_Benutzerprofile",
- "el_GR": "_Προφίλ Χρηστών",
- "en_US": "_User Profiles",
- "es_ES": "_Perfiles de Usuario",
- "fr_FR": "_Profils d'Utilisateurs",
- "he_IL": "_פרופילי משתמש",
- "it_IT": "_Profili utent",
- "ja_JP": "ユーザプロファイル(_M)",
- "ko_KR": "사용자 프로필(_M)",
- "no_NO": "_Brukerprofiler",
- "pl_PL": "_Profile użytkowników",
- "pt_BR": "_Perfis de usuário",
- "ru_RU": "_Учётные записи",
- "sv_SE": "_Användarprofiler",
- "th_TH": "_โปรไฟล์ผู้ใช้งาน",
- "tr_TR": "_Kullanıcı Profilleri",
- "uk_UA": "_Профілі користувачів",
- "zh_CN": "用户配置文件(_M)",
- "zh_TW": "使用者設定檔(_M)"
- }
- },
- {
- "ID": "MenuBarOptionsSimulateWakeUpMessage",
- "Translations": {
- "ar_SA": "محاكاة رسالة الاستيقاظ",
- "de_DE": "Aufwachnachricht simulieren",
- "el_GR": "Προσομοίωση Μηνύματος Αφύπνισης",
- "en_US": "Simulate Wake-Up Message",
- "es_ES": "Simular Mensaje de Reactivación",
- "fr_FR": "Simuler un Message de Réveil",
- "he_IL": "דמה הודעת השכמה",
- "it_IT": "Simula messaggio di risveglio",
- "ja_JP": "スリープ復帰メッセージをシミュレート",
- "ko_KR": "절전 모드 해제 메시지 시뮬레이션",
- "no_NO": "Simuler oppvåknings-melding",
- "pl_PL": "Symuluj wiadomość wybudzania",
- "pt_BR": "Simular Mensagem de Acordar o Console",
- "ru_RU": "Имитировать сообщение пробуждения",
- "sv_SE": "Simulera uppvakningsmeddelande",
- "th_TH": "จำลองการปลุกอุปกรณ์ให้ทำงาน",
- "tr_TR": "Uyandırma Mesajı Simüle Et",
- "uk_UA": "Симулювати повідомлення про пробудження",
- "zh_CN": "模拟唤醒消息",
- "zh_TW": "模擬喚醒訊息"
- }
- },
{
"ID": "MenuBarView",
"Translations": {
@@ -750,31 +575,6 @@
"zh_TW": "圖示"
}
},
- {
- "ID": "GameListHeaderApplication",
- "Translations": {
- "ar_SA": "الاسم",
- "de_DE": "",
- "el_GR": "Όνομα",
- "en_US": "Name",
- "es_ES": "Nombre",
- "fr_FR": "Nom",
- "he_IL": "שם",
- "it_IT": "Nome",
- "ja_JP": "名称",
- "ko_KR": "이름",
- "no_NO": "Navn",
- "pl_PL": "Nazwa",
- "pt_BR": "Nome",
- "ru_RU": "Название",
- "sv_SE": "Namn",
- "th_TH": "ชื่อ",
- "tr_TR": "Oyun Adı",
- "uk_UA": "Назва",
- "zh_CN": "名称",
- "zh_TW": "名稱"
- }
- },
{
"ID": "GameListHeaderDeveloper",
"Translations": {
@@ -1254,7 +1054,7 @@
"ID": "GameListHeaderHostedGames",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Gehostete Spiele: {0}",
"el_GR": "",
"en_US": "Hosted Games: {0}",
"es_ES": "Juegos Alojados: {0}",
@@ -1279,7 +1079,7 @@
"ID": "GameListHeaderPlayerCount",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Spieler online: {0}",
"el_GR": "",
"en_US": "Online Players: {0}",
"es_ES": "Jugadores en Linea: {0}",
@@ -1754,7 +1554,7 @@
"ID": "GameListContextMenuExtractDataAocRomFS",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "DLC-RomFS",
"el_GR": "",
"en_US": "DLC RomFS",
"es_ES": null,
@@ -1779,7 +1579,7 @@
"ID": "GameListContextMenuExtractDataAocRomFSToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Extrahiert die RomFS-Dateien aus einer ausgewählten DLC-Datei.",
"el_GR": "",
"en_US": "Extract the RomFS from a selected DLC file.",
"es_ES": "Extraer el RomFS desde un archivo DLC.",
@@ -2175,56 +1975,6 @@
"zh_TW": "開啟此應用程式模組的另一個 SD 卡 Atmosphère 資料夾。適用於為真實硬體封裝的模組。"
}
},
- {
- "ID": "GameListContextMenuTrimXCI",
- "Translations": {
- "ar_SA": "",
- "de_DE": "XCI-Datei prüfen & trimmen",
- "el_GR": "",
- "en_US": "Check & Trim XCI File",
- "es_ES": "Verificar & Recortar Archivo XCI",
- "fr_FR": "Vérifier & Réduire le Fichier XCI",
- "he_IL": "",
- "it_IT": "Controlla & Riduci il File XCI",
- "ja_JP": "",
- "ko_KR": "XCI 파일 확인 & 트림",
- "no_NO": "Kontroller & trim XCI-filen",
- "pl_PL": "",
- "pt_BR": "Verificar & Reduzir o Arquivo XCI",
- "ru_RU": "Проверить & обрезать XCI файл",
- "sv_SE": "Kontrollera & optimera XCI-fil",
- "th_TH": "ตรวจสอบ & ลดขนาดไฟล์ XCI",
- "tr_TR": "",
- "uk_UA": "Перевірка & нарізка XCI Файлу",
- "zh_CN": "检查并精简 XCI 文件",
- "zh_TW": "檢查 & 修剪 XCI 檔案"
- }
- },
- {
- "ID": "GameListContextMenuTrimXCIToolTip",
- "Translations": {
- "ar_SA": "",
- "de_DE": "Trimmt und prüft die XCI-Datei um Speicherplatz zu sparen.",
- "el_GR": "",
- "en_US": "Check and Trim XCI File to Save Disk Space",
- "es_ES": "Verificar y Recortar Archivo XCI para Ahorrar Espacio en Disco",
- "fr_FR": "Vérifier et Réduire le Fichier XCI pour Économiser de l’Espace Disque",
- "he_IL": "",
- "it_IT": "Controlla e riduci la dimensione del file XCI per risparmiare spazio su disco",
- "ja_JP": "",
- "ko_KR": "디스크 공간을 절약하기 위해 XCI 파일 확인 및 트림",
- "no_NO": "Kontroller og trimm XCI-filen for å spare diskplass",
- "pl_PL": "",
- "pt_BR": "Verifique e reduza o arquivo XCI para economizar espaço em disco",
- "ru_RU": "Проверить и обрезать XCI файл для экономии места на диске",
- "sv_SE": "Kontrollera och optimera XCI-fil för att spara diskutrymme",
- "th_TH": "ตรวจสอบและลดขนาดไฟล์ XCI เพื่อประหยัดเนื้อที่เก็บข้อมูล",
- "tr_TR": "",
- "uk_UA": "Перевірити та обрізати XCI Файл задля збереження місця на диску",
- "zh_CN": "检查并瘦身 XCI 文件以节约磁盘空间",
- "zh_TW": "檢查及修剪 XCI 檔案以節省儲存空間"
- }
- },
{
"ID": "StatusBarGamesLoaded",
"Translations": {
@@ -2250,31 +2000,6 @@
"zh_TW": "{0}/{1} 遊戲已載入"
}
},
- {
- "ID": "StatusBarXCIFileTrimming",
- "Translations": {
- "ar_SA": "",
- "de_DE": "XCI-Datei trimmen: '{0}'",
- "el_GR": "",
- "en_US": "Trimming XCI File '{0}'",
- "es_ES": "Recortando el Siguiente Archivo XCI: '{0}'",
- "fr_FR": "Réduction du Fichier XCI '{0}'",
- "he_IL": "",
- "it_IT": "Riduzione della dimensione del file XCI '{0}'",
- "ja_JP": "",
- "ko_KR": "XCI 파일 '{0}' 트리밍",
- "no_NO": "Trimming av XCI-filen '{0}'",
- "pl_PL": "",
- "pt_BR": "Reduzindo o Arquivo XCI '{0}'",
- "ru_RU": "Обрезается XCI файл '{0}'",
- "sv_SE": "Optimerar XCI-filen '{0}'",
- "th_TH": "กำลังลดขนาดไฟล์ XCI '{0}'",
- "tr_TR": "",
- "uk_UA": "Обрізається XCI Файлів '{0}'",
- "zh_CN": "正在瘦身 XCI 文件 '{0}'",
- "zh_TW": "正在修剪 XCI 檔案 '{0}'"
- }
- },
{
"ID": "LinuxVmMaxMapCountDialogTitle",
"Translations": {
@@ -3904,7 +3629,7 @@
"ID": "SettingsTabSystemEnablePptc",
"Translations": {
"ar_SA": "PPTC (ذاكرة التخزين المؤقت للترجمة المستمرة)",
- "de_DE": "",
+ "de_DE": "PPTC (Profilierter persistenter Übersetzungs-Cache)",
"el_GR": "Ενεργοποίηση PPTC (Profiled Persistent Translation Cache)",
"en_US": "PPTC (Profiled Persistent Translation Cache)",
"es_ES": "PPTC (Cache de Traducción de Perfil Persistente)",
@@ -3979,7 +3704,7 @@
"ID": "SettingsTabSystemTurboMultiplierValueToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Multiplikator-Zielwert für den Turbomodus.\n\nIm Zweifelsfall auf 200 lassen.",
"el_GR": "",
"en_US": "The Turbo mode multiplier target value.\n\nLeave at 200 if unsure.",
"es_ES": "Valor objetivo del multiplicador del modo turbo.\n\nDéjar en 200 si no está seguro.",
@@ -4004,7 +3729,7 @@
"ID": "SettingsTabSystemTurboMultiplierToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Der Turbo-Modus ist eine Emulator-Funktion, die das Spiel beschleunigt oder verlangsamt, wenn es nicht von der Bildrate abhängt.\nDu kannst diese Funktion im Spiel mit einem Hotkey umschalten, der in den Ryujinx Tastatur Hotkeys-Einstellungen konfigurierbar ist.\n\nIm Zweifelsfall auf 200 lassen.",
"el_GR": "",
"en_US": "Turbo Mode is an emulator feature which effectively causes speed up or slow down when a game is not frame-rate sensitive.\nYou can toggle this feature in-game with a hotkey, configurable in Ryujinx Keyboard Hotkeys settings.\n\nLeave at 200 if unsure.",
"es_ES": "El modo turbo es una función del emulador que permite acelerar o ralentizar el juego cuando no depende de la tasa de frames.\nPuedes activar o desactivar esta función durante el juego usando una tecla rápida, configurable en los ajustes de Teclas rápidas de Ryujinx.\n\nDéjar en 200 si no está seguro.",
@@ -4554,7 +4279,7 @@
"ID": "SettingsTabGraphicsAnisotropicFilteringAuto",
"Translations": {
"ar_SA": "تلقائي",
- "de_DE": "",
+ "de_DE": "Automatisch",
"el_GR": "Αυτόματο",
"en_US": "Auto",
"es_ES": "Automático",
@@ -5279,7 +5004,7 @@
"ID": "SettingsTabLoggingEnableAvaloniaLogs",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "UI-Logs aktivieren",
"el_GR": "",
"en_US": "Enable UI Logs",
"es_ES": "Habilitar Registros UI",
@@ -5354,7 +5079,7 @@
"ID": "SettingsTabLoggingEnableNetLogs",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Netzwerk-Logs aktivieren",
"el_GR": "",
"en_US": "Enable Net Logs",
"es_ES": "Habilitar Registros de Red.",
@@ -5800,31 +5525,6 @@
"zh_TW": "確定"
}
},
- {
- "ID": "SettingsButtonCancel",
- "Translations": {
- "ar_SA": "إلغاء",
- "de_DE": "Abbrechen",
- "el_GR": "Ακύρωση",
- "en_US": "Cancel",
- "es_ES": "Cancelar",
- "fr_FR": "Annuler",
- "he_IL": "ביטול",
- "it_IT": "Annulla",
- "ja_JP": "キャンセル",
- "ko_KR": "취소",
- "no_NO": "Avbryt",
- "pl_PL": "Anuluj",
- "pt_BR": "Cancelar",
- "ru_RU": "Отмена",
- "sv_SE": "Avbryt",
- "th_TH": "ยกเลิก",
- "tr_TR": "İptal",
- "uk_UA": "Скасувати",
- "zh_CN": "取消",
- "zh_TW": "取消"
- }
- },
{
"ID": "SettingsButtonApply",
"Translations": {
@@ -6079,7 +5779,7 @@
"ID": "ControllerSettingsHandheld",
"Translations": {
"ar_SA": "محمول",
- "de_DE": "",
+ "de_DE": "Handheld Modus",
"el_GR": "Χειροκίνητο",
"en_US": "Handheld",
"es_ES": "Portátil",
@@ -6129,7 +5829,7 @@
"ID": "ControllerSettingsWaitingConnectDevice",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Konfiguration gefunden:\n\nName:\t{0}\nGUID:\t{1}\n\n Warten auf Verbindung des Controllers...",
"el_GR": "",
"en_US": "Configuration found:\n\nName:\t{0}\nGUID:\t{1}\n\n Waiting for controller connection...",
"es_ES": "Configuración encontrada:\n\nNombre:\t{0}\nGUID:\t{1}\n\nEsperando la conexión del controlador...",
@@ -6179,7 +5879,7 @@
"ID": "ControllerSettingsResetKeybindsToDefault",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Tastenzuordnungen auf Standard zurücksetzen",
"el_GR": "",
"en_US": "Reset keybinds to default",
"es_ES": "Restablecer asignaciones por defecto",
@@ -6200,6 +5900,206 @@
"zh_TW": ""
}
},
+ {
+ "ID": "ControllerSettingsDynamicInputSwap",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Dynamischer Eingabewechsel",
+ "el_GR": "",
+ "en_US": "Dynamic Input Swap",
+ "es_ES": "Cambio Dinámico de Entrada",
+ "fr_FR": "Bascule Dynamique des Périphériques",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ControllerSettingsDynamicInputSwapTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Ermöglicht das Wechseln zwischen verbundenen Eingabegeräten in Echtzeit. Wenn aktiviert, können diesem Spieler-Slot bestimmte Geräte zugewiesen werden, um Eingabekonflikte zwischen Spielern zu vermeiden.",
+ "el_GR": "",
+ "en_US": "Allows real-time switching between connected input devices. When enabled, you can assign specific devices to this player slot to prevent input interference between players.",
+ "es_ES": "Permite cambiar en tiempo real entre dispositivos de entrada conectados. Cuando está activado, puedes asignar dispositivos específicos a este espacio de jugador para evitar interferencias de entrada entre jugadores.",
+ "fr_FR": "Permet de basculer en temps réel entre les périphériques connectés. Lorsqu'il est activé, vous pouvez assigner des périphériques spécifiques à cet emplacement de joueur pour éviter les interférences entre les joueurs.",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "DialogDynamicInputSwapDeviceAssignmentsHint",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Alle aktuell verbundenen Eingabegeräte wurden den folgenden Spielern zugewiesen:\n\n{0}\n\nDu kannst Geräte bestimmten Spielern zuweisen, indem du auf das Zahnradsymbol neben jedem Eingabegerät klickst.",
+ "el_GR": "",
+ "en_US": "All currently connected input devices have been assigned to the following Players:\n\n{0}\n\nYou can assign devices to specific players by clicking the gear icon next to each input device.",
+ "es_ES": "Todos los dispositivos de entrada conectados actualmente se han asignado a los siguientes jugadores:\n\n{0}\n\nPuedes asignar dispositivos a jugadores específicos haciendo clic en el icono de engranaje junto a cada dispositivo de entrada.",
+ "fr_FR": "Tous les périphériques d'entrée actuellement connectés ont été assignés aux joueurs suivants :\n\n{0}\n\nVous pouvez assigner des périphériques à des joueurs spécifiques en cliquant sur l'icône d'engrenage à côté de chaque périphérique d'entrée.",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "DialogDontShowAgain",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Diese Nachricht nicht mehr anzeigen",
+ "el_GR": "",
+ "en_US": "Don't show this message again",
+ "es_ES": "No mostrar este mensaje de nuevo",
+ "fr_FR": "Ne plus afficher ce message",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ControllerSettingsAssignedInputDevices",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Diese Nachricht nicht mehr anzeigen",
+ "el_GR": "",
+ "en_US": "Assigned Devices",
+ "es_ES": "Dispositivos asignados",
+ "fr_FR": "Périphériques assignés",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ControllerSettingsAssignedInputDevicesTooltip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Öffne die Einstellungen für zugewiesene Geräte, um den dynamischen Eingabewechsel sowie die gerätebezogenen Profilzuweisungen für diesen Spieler zu konfigurieren.",
+ "el_GR": "",
+ "en_US": "Open the Assigned Devices settings to configure Dynamic Input Swap and per-device profile bindings for this player.",
+ "es_ES": "Abre la configuración de dispositivos asignados para configurar el Cambio Dinámico de Entrada y las vinculaciones de perfil por dispositivo para este jugador.",
+ "fr_FR": "Ouvre les paramètres des périphériques assignés pour configurer la Bascule Dynamique des Périphériques et les liaisons de profil par périphérique pour ce joueur.",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ControllerSettingsAllowDuplicateDeviceAssignment",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Dasselbe Gerät für mehrere Spieler erlauben",
+ "el_GR": "",
+ "en_US": "Allow the same device for multiple players",
+ "es_ES": "Permitir el mismo dispositivo para varios jugadores",
+ "fr_FR": "Autoriser le même périphérique pour plusieurs joueurs",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "ControllerSettingsBindProfileToolTip",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Dieses Profil als Standard für das aktuell verbundene Eingabegerät festlegen. Das verknüpfte Profil wird automatisch geladen, wenn dieses Gerät verbunden oder über den dynamischen Eingabetausch gewechselt wird.",
+ "el_GR": "",
+ "en_US": "Link this profile as the default for the currently connected input device. The linked profile will load automatically when this device is connected or swapped via Dynamic Input Swap.",
+ "es_ES": "Vincular este perfil como predeterminado para el dispositivo de entrada actualmente conectado. El perfil vinculado se cargará automáticamente cuando se conecte este dispositivo o se intercambie mediante Intercambio Dinámico de Entrada.",
+ "fr_FR": "Lier ce profil comme profil par défaut pour le périphérique d'entrée actuellement connecté. Le profil lié sera chargé automatiquement lorsque ce périphérique sera connecté ou échangé via l'Échange Dynamique d'Entrée.",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
{
"ID": "ControllerSettingsDeviceDisabled",
"Translations": {
@@ -6254,7 +6154,7 @@
"ID": "ControllerSettingsControllerTypeHandheld",
"Translations": {
"ar_SA": "محمول",
- "de_DE": "",
+ "de_DE": "Handheld-Modus",
"el_GR": "Φορητό",
"en_US": "Handheld",
"es_ES": "Portátil",
@@ -7704,7 +7604,7 @@
"ID": "ControllerSettingsMotionServerHost",
"Translations": {
"ar_SA": "مضيف الخادم:",
- "de_DE": "",
+ "de_DE": "Server-Host:",
"el_GR": "Κεντρικός Υπολογιστής Διακομιστή:",
"en_US": "Server Host:",
"es_ES": "Host del Servidor:",
@@ -7804,7 +7704,7 @@
"ID": "ControllerSettingsLedColorDisable",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Deaktivieren",
"el_GR": "",
"en_US": "Disable",
"es_ES": "Desactivar",
@@ -7829,7 +7729,7 @@
"ID": "ControllerSettingsLedColorRainbow",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Regenbogen",
"el_GR": "",
"en_US": "Rainbow",
"es_ES": "Arcoíris",
@@ -7854,7 +7754,7 @@
"ID": "ControllerSettingsLedColorRainbowSpeed",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Regenbogen-Geschwindigkeit:",
"el_GR": "",
"en_US": "Rainbow Speed:",
"es_ES": "Velocidad del arcoíris:",
@@ -7879,7 +7779,7 @@
"ID": "ControllerSettingsLedColor",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Farbe",
"el_GR": "",
"en_US": "Color",
"es_ES": null,
@@ -8004,7 +7904,7 @@
"ID": "GamepadLeftShoulder",
"Translations": {
"ar_SA": "زر الكتف الأيسر L",
- "de_DE": "",
+ "de_DE": "Linke Schulter",
"el_GR": "",
"en_US": "Left Shoulder",
"es_ES": "Botón Superior Izq.",
@@ -8029,7 +7929,7 @@
"ID": "GamepadRightShoulder",
"Translations": {
"ar_SA": "زر الكتف الأيمن R",
- "de_DE": "",
+ "de_DE": "Rechte Schulter",
"el_GR": "",
"en_US": "Right Shoulder",
"es_ES": "Botón Superior Derecho",
@@ -8054,7 +7954,7 @@
"ID": "GamepadLeftTrigger",
"Translations": {
"ar_SA": "زر الزناد الأيسر (ZL)",
- "de_DE": "",
+ "de_DE": "Linker Trigger",
"el_GR": "",
"en_US": "Left Trigger",
"es_ES": "Gatillo Izquierdo",
@@ -8079,7 +7979,7 @@
"ID": "GamepadRightTrigger",
"Translations": {
"ar_SA": "زر الزناد الأيمن (ZR)",
- "de_DE": "",
+ "de_DE": "Rechter Trigger",
"el_GR": "",
"en_US": "Right Trigger",
"es_ES": "Gatillo Derecho",
@@ -8104,7 +8004,7 @@
"ID": "GamepadDpadUp",
"Translations": {
"ar_SA": "فوق",
- "de_DE": "",
+ "de_DE": "Hoch",
"el_GR": "",
"en_US": "Up",
"es_ES": "Arriba",
@@ -8129,7 +8029,7 @@
"ID": "GamepadDpadDown",
"Translations": {
"ar_SA": "اسفل",
- "de_DE": "",
+ "de_DE": "Runter",
"el_GR": "",
"en_US": "Down",
"es_ES": "Abajo",
@@ -8154,7 +8054,7 @@
"ID": "GamepadDpadLeft",
"Translations": {
"ar_SA": "يسار",
- "de_DE": "",
+ "de_DE": "Links",
"el_GR": "",
"en_US": "Left",
"es_ES": "Izquierda",
@@ -8179,7 +8079,7 @@
"ID": "GamepadDpadRight",
"Translations": {
"ar_SA": "يمين",
- "de_DE": "",
+ "de_DE": "Rechts",
"el_GR": "",
"en_US": "Right",
"es_ES": "Derecha",
@@ -8266,7 +8166,7 @@
"no_NO": "Veiledning",
"pl_PL": "",
"pt_BR": "Guia",
- "ru_RU": "Кнопка меню",
+ "ru_RU": "Меню",
"sv_SE": null,
"th_TH": "คู่มือ",
"tr_TR": "Rehber",
@@ -8279,7 +8179,7 @@
"ID": "GamepadMisc1",
"Translations": {
"ar_SA": "متنوع",
- "de_DE": "",
+ "de_DE": "Sonstige",
"el_GR": "",
"en_US": "Misc",
"es_ES": "Otros",
@@ -8429,7 +8329,7 @@
"ID": "GamepadSingleLeftTrigger0",
"Translations": {
"ar_SA": "زر الزناد الأيسر 0",
- "de_DE": "",
+ "de_DE": "Linker Trigger 0",
"el_GR": "",
"en_US": "Left Trigger 0",
"es_ES": "Gatillo Izquierdo 0",
@@ -8454,7 +8354,7 @@
"ID": "GamepadSingleRightTrigger0",
"Translations": {
"ar_SA": "زر الزناد الأيمن 0",
- "de_DE": "",
+ "de_DE": "Rechter Trigger 0",
"el_GR": "",
"en_US": "Right Trigger 0",
"es_ES": "Gatillo Derecho 0",
@@ -8479,7 +8379,7 @@
"ID": "GamepadSingleLeftTrigger1",
"Translations": {
"ar_SA": "زر الزناد الأيسر 1",
- "de_DE": "",
+ "de_DE": "Linker Trigger 1",
"el_GR": "",
"en_US": "Left Trigger 1",
"es_ES": "Gatillo Izquierdo 1",
@@ -8504,7 +8404,7 @@
"ID": "GamepadSingleRightTrigger1",
"Translations": {
"ar_SA": "زر الزناد الأيمن 1",
- "de_DE": "",
+ "de_DE": "Rechter Trigger 1",
"el_GR": "",
"en_US": "Right Trigger 1",
"es_ES": "Gatillo Derecho 1",
@@ -8529,7 +8429,7 @@
"ID": "StickLeft",
"Translations": {
"ar_SA": "عصا التحكم اليسرى",
- "de_DE": "",
+ "de_DE": "Linker Stick",
"el_GR": "",
"en_US": "Left Stick",
"es_ES": "Joystick Izquierdo",
@@ -8554,7 +8454,7 @@
"ID": "StickRight",
"Translations": {
"ar_SA": "عصا التحكم اليمنى",
- "de_DE": "",
+ "de_DE": "Rechter Stick",
"el_GR": "",
"en_US": "Right Stick",
"es_ES": "Joystick Derecho",
@@ -8975,36 +8875,11 @@
"zh_TW": "確定"
}
},
- {
- "ID": "InputDialogCancel",
- "Translations": {
- "ar_SA": "إلغاء",
- "de_DE": "Abbrechen",
- "el_GR": "Ακύρωση",
- "en_US": "Cancel",
- "es_ES": "Cancelar",
- "fr_FR": "Annuler",
- "he_IL": "ביטול",
- "it_IT": "Annulla",
- "ja_JP": "キャンセル",
- "ko_KR": "취소",
- "no_NO": "Avbryt",
- "pl_PL": "Anuluj",
- "pt_BR": "Cancelar",
- "ru_RU": "Отмена",
- "sv_SE": "Avbryt",
- "th_TH": "ยกเลิก",
- "tr_TR": "İptal",
- "uk_UA": "Скасувати",
- "zh_CN": "取消",
- "zh_TW": "取消"
- }
- },
{
"ID": "InputDialogCancelling",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wird abgebrochen",
"el_GR": "",
"en_US": "Cancelling",
"es_ES": "Anulación en Curso",
@@ -9029,7 +8904,7 @@
"ID": "InputDialogClose",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Schließen",
"el_GR": "",
"en_US": "Close",
"es_ES": "Cerrar",
@@ -9229,7 +9104,7 @@
"ID": "ControllerSettingsViewProfileToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Profil ansehen",
"el_GR": "",
"en_US": "View Profile",
"es_ES": "Ver Perfil",
@@ -9579,11 +9454,11 @@
"ID": "ControllerSettingsRumbleUseHDRumble",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "HD-Vibration aktivieren",
"el_GR": "",
"en_US": "Enable HD Rumble",
- "es_ES": "Activa vibración HD",
- "fr_FR": "",
+ "es_ES": "Activa Vibración HD",
+ "fr_FR": "Activer la Vibration HD",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
@@ -9604,11 +9479,11 @@
"ID": "HDRumbleTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "EXPERIMENTELL.\n\nSendet mehr Daten an den Controller für verbesserte Vibration.\n\nAktuell werden nur offizielle Nintendo Switch Controller unterstützt.\n\nIm Zweifelsfall AUS lassen.",
"el_GR": "",
- "en_US": "Sends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave ON if you're using JoyCons or a Pro Controller.",
- "es_ES": "",
- "fr_FR": "",
+ "en_US": "EXPERIMENTAL.\n\nSends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave OFF if unsure.",
+ "es_ES": "EXPERIMENTAL.\n\nEnvía más datos al controlador para una mejor vibración.\n\nActualmente solo soporta controladores de Nintendo Switch de primera parte.\n\nDéjar desactivado si no está seguro.",
+ "fr_FR": "EXPÉRIMENTAL.\n\nEnvoie plus de données au contrôleur pour une meilleure vibration.\n\nPrend actuellement en charge uniquement les manettes Nintendo Switch de première partie.\n\nLaisser désactivé en cas de doute.",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
@@ -10154,7 +10029,7 @@
"ID": "DialogUpdaterConvertFailedServerMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Scheiterte bei der Konvertierung der Ryujinx-Version, die vom Update-Server empfangen wurde.",
"el_GR": "",
"en_US": "Failed to convert the Ryujinx version received from the update server.",
"es_ES": "Error al convertir la versión de Ryujinx recibida del servidor de actualizaciones.",
@@ -10304,7 +10179,7 @@
"ID": "DialogUpdaterShowChangelogMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Changelog anzeigen",
"el_GR": "",
"en_US": "Show Changelog",
"es_ES": "Mostrar Changelog",
@@ -10575,31 +10450,6 @@
"zh_TW": "開啟設定視窗"
}
},
- {
- "ID": "DialogOpenXCITrimmerWindowLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI Trimmer Window",
- "es_ES": "Ventana Recortador XCI",
- "fr_FR": "Fenêtre de Réduction de Fichiers XCI",
- "he_IL": "",
- "it_IT": "Riduci dimensioni dei file XCI",
- "ja_JP": "",
- "ko_KR": "XCI 트리머 창",
- "no_NO": "XCI Trimmervindu",
- "pl_PL": "",
- "pt_BR": "Janela de Redução XCI",
- "ru_RU": "Окно XCI триммера",
- "sv_SE": "XCI-optimerare",
- "th_TH": "หน้าต่างตัดแต่งไฟล์ XCI",
- "tr_TR": "",
- "uk_UA": "Вікно XCI Тримера",
- "zh_CN": "XCI 文件瘦身窗口",
- "zh_TW": "XCI 修剪器視窗"
- }
- },
{
"ID": "DialogControllerAppletTitle",
"Translations": {
@@ -10775,81 +10625,6 @@
"zh_TW": "Ryujinx 錯誤 ({0})"
}
},
- {
- "ID": "DialogAmiiboApiTitle",
- "Translations": {
- "ar_SA": "أميبو API",
- "de_DE": "Amiibo-API",
- "el_GR": "API για Amiibo.",
- "en_US": "Amiibo API",
- "es_ES": "API Amiibo",
- "fr_FR": "API Amiibo",
- "he_IL": "ממשק תכנות אמיבו",
- "it_IT": "API Amiibo",
- "ja_JP": "",
- "ko_KR": null,
- "no_NO": "",
- "pl_PL": "API Amiibo",
- "pt_BR": "API Amiibo",
- "ru_RU": "API Amiibo",
- "sv_SE": "Amiibo-API",
- "th_TH": "อามิโบ้ API",
- "tr_TR": "",
- "uk_UA": "API Amiibo",
- "zh_CN": null,
- "zh_TW": null
- }
- },
- {
- "ID": "DialogAmiiboApiFailFetchMessage",
- "Translations": {
- "ar_SA": "حدث خطأ أثناء جلب المعلومات من API.",
- "de_DE": "Beim Abrufen von Informationen aus der API ist ein Fehler aufgetreten.",
- "el_GR": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση πληροφοριών από το API.",
- "en_US": "An error occured while fetching information from the API.",
- "es_ES": "Ocurrió un error al recibir información de la API.",
- "fr_FR": "Une erreur est survenue lors de la récupération des informations de l'API.",
- "he_IL": "אירעה שגיאה בעת שליפת מידע מהממשק.",
- "it_IT": "Si è verificato un errore durante il recupero delle informazioni dall'API.",
- "ja_JP": "API からの情報取得中にエラーが発生しました.",
- "ko_KR": "API에서 정보를 가져오는 중에 오류가 발생했습니다.",
- "no_NO": "En feil oppstod under henting av informasjon fra API.",
- "pl_PL": "Wystąpił błąd podczas pobierania informacji z API.",
- "pt_BR": "Um erro ocorreu ao tentar obter informações da API.",
- "ru_RU": "Произошла ошибка при получении информации из API.",
- "sv_SE": "Ett fel inträffade vid hämtning av information från API.",
- "th_TH": "เกิดข้อผิดพลาดขณะเรียกข้อมูลจาก API",
- "tr_TR": "API'dan bilgi alırken bir hata oluştu.",
- "uk_UA": "Під час отримання інформації з API сталася помилка.",
- "zh_CN": "从 API 获取信息时出错。",
- "zh_TW": "從 API 取得資訊時出現錯誤。"
- }
- },
- {
- "ID": "DialogAmiiboApiConnectErrorMessage",
- "Translations": {
- "ar_SA": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.",
- "de_DE": "Verbindung zum Amiibo API Server kann nicht hergestellt werden. Der Dienst ist möglicherweise nicht verfügbar oder es existiert keine Internetverbindung.",
- "el_GR": "Δεν είναι δυνατή η σύνδεση με τον διακομιστή Amiibo API. Η υπηρεσία μπορεί να είναι εκτός λειτουργίας ή μπορεί να χρειαστεί να επαληθεύσετε ότι έχετε ενεργή σύνδεσή στο Διαδίκτυο.",
- "en_US": "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.",
- "es_ES": "No se pudo conectar al servidor de la API Amiibo. El servicio puede estar caído o tu conexión a internet puede haberse desconectado.",
- "fr_FR": "Impossible de se connecter au serveur API Amiibo. Le service est peut-être hors service ou vous devriez peut-être vérifier que votre connexion internet est connectée.",
- "he_IL": "לא ניתן להתחבר לממשק שרת האמיבו. ייתכן שהשירות מושבת או שתצטרך לוודא שהחיבור לאינטרנט שלך מקוון.",
- "it_IT": "Impossibile connettersi al server dell'API Amiibo. Il servizio potrebbe non essere disponibile o potresti non essere connesso a Internet.",
- "ja_JP": "Amiibo API サーバに接続できませんでした. サーバがダウンしているか, インターネット接続に問題があるかもしれません.",
- "ko_KR": "Amiibo API 서버에 연결할 수 없습니다. 서비스가 다운되었거나 인터넷 연결이 온라인 상태인지 확인이 필요합니다.",
- "no_NO": "Kan ikke koble til Amiibo API server. Tjenesten kan være nede, eller du må kanskje verifisere at din internettforbindelse er tilkoblet.",
- "pl_PL": "Nie można połączyć się z serwerem API Amiibo. Usługa może nie działać lub może być konieczne sprawdzenie, czy połączenie internetowe jest online.",
- "pt_BR": "Não foi possível conectar ao servidor da API Amiibo. O serviço pode estar fora do ar ou você precisa verificar sua conexão com a Internet.",
- "ru_RU": "Не удалось подключиться к серверу Amiibo API. Служба может быть недоступна или вам может потребоваться проверить ваше интернет-соединение.",
- "sv_SE": "Kunde inte ansluta till Amiibo API-server. Tjänsten kanske är nere eller så behöver du kontrollera att din internetanslutning fungerar.",
- "th_TH": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Amiibo API บางบริการอาจหยุดทำงาน หรือไม่คุณต้องทำการตรวจสอบว่าอินเทอร์เน็ตของคุณอยู่ในสถานะเชื่อมต่ออยู่หรือไม่",
- "tr_TR": "Amiibo API sunucusuna bağlanılamadı. Sunucu çevrimdışı olabilir veya uygun bir internet bağlantınızın olduğunu kontrol etmeniz gerekebilir.",
- "uk_UA": "Неможливо підключитися до сервера Amiibo API. Можливо, служба не працює або вам потрібно перевірити, чи є підключення до Інтернету.",
- "zh_CN": "无法连接到 Amiibo API 服务器,服务可能已关闭,或者没有连接互联网。",
- "zh_TW": "無法連接 Amiibo API 伺服器。服務可能已停機,或者您可能需要確認網際網路連線是否在線上。"
- }
- },
{
"ID": "DialogProfileInvalidProfileErrorMessage",
"Translations": {
@@ -11029,7 +10804,7 @@
"ID": "DialogPPTCNukeMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Du bist kurz davor, alle PPTC-Daten zu löschen von:\n\n{0}\n\nBist du sicher, dass du fortfahren möchtest?",
"el_GR": "",
"en_US": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"es_ES": "Está a punto de eliminar todos los datos PPTC de:\n\n{0}\n\n¿Está seguro de que desea continuar?",
@@ -11300,11 +11075,11 @@
"zh_TW": "目前控制器設定已更新。"
}
},
- {
- "ID": "DialogControllerSettingsModifiedConfirmSubMessage",
- "Translations": {
- "ar_SA": "هل تريد الحفظ ؟",
- "de_DE": "Controller-Einstellungen speichern?",
+ {
+ "ID": "DialogControllerSettingsModifiedConfirmSubMessage",
+ "Translations": {
+ "ar_SA": "هل تريد الحفظ ؟",
+ "de_DE": "Controller-Einstellungen speichern?",
"el_GR": "Θέλετε να αποθηκεύσετε;",
"en_US": "Do you want to save?",
"es_ES": "¿Guardar cambios?",
@@ -11321,63 +11096,63 @@
"th_TH": "คุณต้องการบันทึกหรือไม่?",
"tr_TR": "Kaydetmek istiyor musunuz?",
"uk_UA": "Ви хочете зберегти?",
- "zh_CN": "是否保存?",
- "zh_TW": "您想要儲存嗎?"
- }
- },
- {
- "ID": "DialogControllerSettingsResetKeybindsConfirmMessage",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Reset the selected device keybinds to their default values?",
- "es_ES": "¿Restablecer las asignaciones del dispositivo seleccionado a sus valores predeterminados?",
- "fr_FR": "Réinitialiser les assignations du périphérique sélectionné à leurs valeurs par défaut ?",
- "he_IL": "",
- "it_IT": "",
- "ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": "",
- "ru_RU": "",
- "sv_SE": "",
- "th_TH": "",
- "tr_TR": "",
- "uk_UA": "",
- "zh_CN": "要将选定设备的按键绑定重置为默认值吗?",
- "zh_TW": ""
- }
- },
- {
- "ID": "DialogControllerSettingsResetKeybindsConfirmSubMessage",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "This is a destructive action and will overwrite the current bindings for this device.",
- "es_ES": "Esta acción es destructiva y sobrescribirá las asignaciones actuales de este dispositivo.",
- "fr_FR": "Cette action est destructive et écrasera les assignations actuelles de ce périphérique.",
- "he_IL": "",
- "it_IT": "",
- "ja_JP": "",
- "ko_KR": "",
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": "",
- "ru_RU": "",
- "sv_SE": "",
- "th_TH": "",
- "tr_TR": "",
- "uk_UA": "",
- "zh_CN": "这是一个破坏性的操作,会覆盖此设备当前的绑定。",
- "zh_TW": ""
- }
- },
- {
- "ID": "DialogLoadFileErrorMessage",
- "Translations": {
+ "zh_CN": "是否保存?",
+ "zh_TW": "您想要儲存嗎?"
+ }
+ },
+ {
+ "ID": "DialogControllerSettingsResetKeybindsConfirmMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Reset the selected device keybinds to their default values?",
+ "es_ES": "¿Restablecer las asignaciones del dispositivo seleccionado a sus valores predeterminados?",
+ "fr_FR": "Réinitialiser les assignations du périphérique sélectionné à leurs valeurs par défaut ?",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "要将选定设备的按键绑定重置为默认值吗?",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "DialogControllerSettingsResetKeybindsConfirmSubMessage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "This is a destructive action and will overwrite the current bindings for this device.",
+ "es_ES": "Esta acción es destructiva y sobrescribirá las asignaciones actuales de este dispositivo.",
+ "fr_FR": "Cette action est destructive et écrasera les assignations actuelles de ce périphérique.",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "这是一个破坏性的操作,会覆盖此设备当前的绑定。",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "DialogLoadFileErrorMessage",
+ "Translations": {
"ar_SA": "{0}. ملف خاطئ: {1}",
"de_DE": "{0}. Fehlerhafte Datei: {1}",
"el_GR": "",
@@ -11604,7 +11379,7 @@
"ID": "NotificationLaunchCheckDramSizeTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "{0} DRAM aktiviert",
"el_GR": "",
"en_US": "{0} DRAM Enabled",
"es_ES": "{0} DRAM Habilitada",
@@ -11629,7 +11404,7 @@
"ID": "NotificationLaunchCheckDramSizeMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Die Verwendung von mehr als 4 GiB DRAM kann bei einigen Anwendungen zu Abstürzen führen.",
"el_GR": "",
"en_US": "Using above 4GiB DRAM may cause crashes in some applications.",
"es_ES": "Usar más de 4 GiB de DRAM puede causar fallos en algunas aplicaciones.",
@@ -11654,7 +11429,7 @@
"ID": "NotificationLaunchCheckGdbStubTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Debug: GDB Stub aktiviert (Port: {0})",
"el_GR": "",
"en_US": "Debug: GDB Stub Enabled (Port: {0})",
"es_ES": "Debug: GDB Stub Habilitado (Puerto: {0})",
@@ -11679,7 +11454,7 @@
"ID": "NotificationLaunchCheckGdbStubMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Dies wird die Leistung beeinträchtigen.",
"el_GR": "",
"en_US": "This will affect performance.",
"es_ES": "Esto afectará el rendimiento.",
@@ -11704,7 +11479,7 @@
"ID": "NotificationLaunchCheckSuspendOnStartTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Beim Start anhalten aktiviert",
"el_GR": "",
"en_US": "Debug: Suspend on Start Enabled",
"es_ES": "Debug: Suspender al Inicio Habilitado",
@@ -11729,7 +11504,7 @@
"ID": "NotificationLaunchCheckSuspendOnStartMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Die Anwendung wurde angehalten. Zum Fortfahren einen Debugger anhängen.",
"el_GR": "",
"en_US": "Application has been suspended. Attach a debugger to continue.",
"es_ES": "La aplicación ha sido suspendida. Adjunte un depurador para continuar.",
@@ -12279,7 +12054,7 @@
"ID": "AboutRyujinxAboutContent",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Ryujinx ist ein Emulator für the Nintendo Switch™ 1.\nErhalte alle aktuellen Neuigkeiten in unserem Discord.\nEntwickler, die an einer Mitarbeit interessiert sind, finden weitere Informationen auf unserem Forgejo oder Discord..",
"el_GR": "",
"en_US": "Ryujinx is an emulator for the Nintendo Switch™ 1.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our Forgejo or Discord.",
"es_ES": "Ryujinx es un emulador para Nintendo Switch™ 1.\nObtén todas las novedades en nuestro Discord.\nLos desarrolladores interesados en contribuir pueden obtener más información en nuestro Forgejo o Discord.",
@@ -12329,7 +12104,7 @@
"ID": "AboutRyujinxFormerMaintainersTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Zuvor Entwickelt von:",
"el_GR": "",
"en_US": "Formerly Maintained By:",
"es_ES": "Anteriormente Mantenido por:",
@@ -12375,131 +12150,6 @@
"zh_TW": "在預設瀏覽器中開啟貢獻者的網頁"
}
},
- {
- "ID": "AmiiboSeriesLabel",
- "Translations": {
- "ar_SA": "مجموعة أميبو",
- "de_DE": "Amiibo-Serie",
- "el_GR": "Σειρά Amiibo",
- "en_US": "Amiibo Series",
- "es_ES": "Serie de Amiibo",
- "fr_FR": "Séries Amiibo",
- "he_IL": "סדרת אמיבו",
- "it_IT": "Serie Amiibo",
- "ja_JP": "Amiibo シリーズ",
- "ko_KR": "Amiibo 시리즈",
- "no_NO": "Amibo Serie",
- "pl_PL": "Seria Amiibo",
- "pt_BR": "Franquia Amiibo",
- "ru_RU": "Серия Amiibo",
- "sv_SE": "Amiibo-serie",
- "th_TH": "ชุดซีรีส์ Amiibo",
- "tr_TR": "Amiibo Serisi",
- "uk_UA": "Серія Amiibo",
- "zh_CN": "Amiibo 系列",
- "zh_TW": "Amiibo 系列"
- }
- },
- {
- "ID": "AmiiboCharacterLabel",
- "Translations": {
- "ar_SA": "شخصية",
- "de_DE": "Charakter",
- "el_GR": "Χαρακτήρας",
- "en_US": "Character",
- "es_ES": "Personaje",
- "fr_FR": "Personnage",
- "he_IL": "דמות",
- "it_IT": "Personaggio",
- "ja_JP": "キャラクタ",
- "ko_KR": "캐릭터",
- "no_NO": "Karakter",
- "pl_PL": "Postać",
- "pt_BR": "Personagem",
- "ru_RU": "Персонаж",
- "sv_SE": "Karaktär",
- "th_TH": "ตัวละคร",
- "tr_TR": "Karakter",
- "uk_UA": "Персонаж",
- "zh_CN": "角色",
- "zh_TW": "角色"
- }
- },
- {
- "ID": "AmiiboScanButtonLabel",
- "Translations": {
- "ar_SA": "فحصه",
- "de_DE": "Einscannen",
- "el_GR": "Σαρώστε το",
- "en_US": "Scan It",
- "es_ES": "Escanear",
- "fr_FR": "Scanner",
- "he_IL": "סרוק את זה",
- "it_IT": "Scansiona",
- "ja_JP": "スキャン",
- "ko_KR": "스캔하기",
- "no_NO": "Skanne det",
- "pl_PL": "Zeskanuj",
- "pt_BR": "Escanear",
- "ru_RU": "Сканировать",
- "sv_SE": "Skanna den",
- "th_TH": "สแกนเลย",
- "tr_TR": "Tarat",
- "uk_UA": "Сканувати",
- "zh_CN": "扫描",
- "zh_TW": "掃描"
- }
- },
- {
- "ID": "AmiiboOptionsShowAllLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Show incompatible Amiibo for this game",
- "es_ES": "Mostrar Amiibo incompatibles con este juego",
- "fr_FR": "Montrer les Amiibo incompatibles avec ce jeu",
- "he_IL": "",
- "it_IT": "Mostra Amiibo compatibili per questo gioco",
- "ja_JP": "",
- "ko_KR": "이 게임에 호환되지 않는 Amiibo 표시",
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": "Mostar Amiibos incompatíveis para este jogo.",
- "ru_RU": "",
- "sv_SE": "Visa Amiibo som inte är kompatibla med detta spel",
- "th_TH": "",
- "tr_TR": "",
- "uk_UA": "",
- "zh_CN": "显示与此游戏不兼容的 Amiibo",
- "zh_TW": "顯示與此遊戲不相容的 Amiibo"
- }
- },
- {
- "ID": "AmiiboOptionsUsRandomTagLabel",
- "Translations": {
- "ar_SA": "هاك: استخدم علامة Uuid عشوائية ",
- "de_DE": "Hack: Benutze zufällige Tag-UUID",
- "el_GR": "Hack: Χρησιμοποιήστε τυχαίο αναγνωριστικό UUID",
- "en_US": "Hack: Use Random tag Uuid",
- "es_ES": "Hack: Usar Etiqueta Aleatoria Uuid",
- "fr_FR": "Hack : Utiliser un tag Uuid Aléatoire",
- "he_IL": "האצה: השתמש בתג Uuid אקראי",
- "it_IT": "Espediente: Usa un UUID del tag casuale",
- "ja_JP": "ハック: ランダムな Uuid を使用",
- "ko_KR": "핵 : 무작위 태그 Uuid 사용",
- "no_NO": "Hack: Bruk tilfeldig tag-Uuid",
- "pl_PL": "Hack: Użyj losowego UUID tagu",
- "pt_BR": "Hack: Usar Uuid de tag Aleatório",
- "ru_RU": "Хак: Использовать случайный тег Uuid",
- "sv_SE": "Hack: Använd slumpmässig tagg för Uuid",
- "th_TH": "แฮ็ค: สุ่มแท็ก Uuid",
- "tr_TR": "Hack: Rastgele bir Uuid kullan",
- "uk_UA": "Хитрість: Використовувати випадковий тег Uuid",
- "zh_CN": "修改:使用随机生成的 Amiibo ID",
- "zh_TW": "補釘修正:使用隨機標記的 Uuid"
- }
- },
{
"ID": "DlcManagerTableHeadingEnabledLabel",
"Translations": {
@@ -12800,56 +12450,6 @@
"zh_TW": "我的最愛"
}
},
- {
- "ID": "OrderAscending",
- "Translations": {
- "ar_SA": "تصاعدي",
- "de_DE": "Aufsteigend",
- "el_GR": "Αύξουσα",
- "en_US": "Ascending",
- "es_ES": "Ascendente",
- "fr_FR": "Croissant",
- "he_IL": "סדר עולה",
- "it_IT": "Crescente",
- "ja_JP": "昇順",
- "ko_KR": "오름차순",
- "no_NO": "Stigende",
- "pl_PL": "Rosnąco",
- "pt_BR": "Ascendente",
- "ru_RU": "По Возрастанию",
- "sv_SE": "Stigande",
- "th_TH": "จากน้อยไปมาก",
- "tr_TR": "Artan",
- "uk_UA": "За зростанням",
- "zh_CN": "升序",
- "zh_TW": "從小到大"
- }
- },
- {
- "ID": "OrderDescending",
- "Translations": {
- "ar_SA": "تنازلي",
- "de_DE": "Absteigend",
- "el_GR": "Φθίνουσα",
- "en_US": "Descending",
- "es_ES": "Descendente",
- "fr_FR": "Décroissant",
- "he_IL": "סדר יורד",
- "it_IT": "Decrescente",
- "ja_JP": "降順",
- "ko_KR": "내림차순",
- "no_NO": "Synkende",
- "pl_PL": "Malejąco",
- "pt_BR": "Descendente",
- "ru_RU": "По Убыванию",
- "sv_SE": "Fallande",
- "th_TH": "จากมากไปน้อย",
- "tr_TR": "Azalan",
- "uk_UA": "За спаданням",
- "zh_CN": "降序",
- "zh_TW": "從大到小"
- }
- },
{
"ID": "SettingsTabGraphicsFeatures",
"Translations": {
@@ -13029,7 +12629,7 @@
"ID": "UseGlobalInputTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Wenn diese Option in benutzerdefinierten Einstellungen aktiviert ist, wird die globale Eingabekonfiguration verwendet.\n\nIn den globalen Einstellungen kann sie nach Bedarf aktiviert oder deaktiviert werden; diese Einstellung wird von allen neu erstellten benutzerdefinierten Konfigurationen übernommen.",
"el_GR": "",
"en_US": "If this option is enabled in custom settings, the global input configuration will be used.\n\nIn the global settings: you can enable or disable it as needed; this setting will be inherited by any new custom configurations created.",
"es_ES": "Si esta opción está activada en los ajustes personalizados, se utilizará la configuración de entrada global.\n\nEn los ajustes globales: puedes activarla o desactivarla según sea necesario; esta configuración se aplicará a cualquier nueva configuración personalizada que se cree.",
@@ -13129,7 +12729,7 @@
"ID": "MatchTimeTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Systemzeit mit dem aktuellen Datum und der Uhrzeit des PCs synchronisieren.",
"el_GR": "",
"en_US": "Sync System Time to match your PC's current date & time.",
"es_ES": "Sincronizar la hora del sistema con la fecha y hora actual de tu PC.",
@@ -13304,11 +12904,11 @@
"ID": "GCLowLatencyTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Versetzt den Garbage Collector der CLR in den Niedriglatenz-Modus.\n\nDies kann Ruckler verringern, allerdings auf Kosten der Leistung.\n\nIm Zweifelsfall AUS lassen.",
"el_GR": "",
"en_US": "Sets the garbage collector for the CLR to low-latency mode.\n\nThis may decrease stuttering at the cost of performance.\n\nLeave OFF if unsure.",
- "es_ES": "",
- "fr_FR": "",
+ "es_ES": "Configura el recolector de basura del CLR en modo baja latencia.\n\nEsto puede reducir los tartamudeos a costa del rendimiento.\n\nDéjar desactivado si no está seguro.",
+ "fr_FR": "Configure le Collecteur de Déchets du CLR en mode faible latence.\n\nCela peut réduire les saccades, au détriment des performances.\n\nLaisser désactivé en cas de doute.",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
@@ -13479,7 +13079,7 @@
"ID": "IgnoreControllerAppletTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Der Controller-Applet-Dialog wird nicht angezeigt, wenn das Gamepad während einer laufenden Anwendung getrennt wird.\n\nIm Zweifelsfall AUS lassen.",
"el_GR": "",
"en_US": "The Controller Applet dialog will not appear if the gamepad is disconnected while an application is running.\n\nLeave OFF if unsure.",
"es_ES": "El cuadro de diálogo del Applet de controladores no aparecerá si el gamepad se desconecta mientras una aplicación está en ejecución.\n\nDéjar DESACTIVADO si no está seguro.",
@@ -13554,7 +13154,7 @@
"ID": "GalThreadingTooltip",
"Translations": {
"ar_SA": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.",
- "de_DE": "Führt Grafik-Backend Befehle auf einem zweiten Thread aus.\n\nDies Beschleunigt die Shader-Kompilierung, reduziert Stottern und verbessert die Leistung auf GPU-Treibern ohne eigene Multithreading-Unterstützung. Geringfügig bessere Leistung bei Treibern mit Multithreading.\n\nIm Zweifelsfall auf auf AUTO stellen.",
+ "de_DE": "Führt Grafik-Backend Befehle auf einem zweiten Thread aus.\n\nDies Beschleunigt die Shader-Kompilierung, reduziert Stottern und verbessert die Leistung auf GPU-Treibern ohne eigene Multithreading-Unterstützung. Geringfügig bessere Leistung bei Treibern mit Multithreading.\n\nIm Zweifelsfall auf AUTO stellen.",
"el_GR": "Εκτελεί εντολές γραφικών σε ένα δεύτερο νήμα. Επιτρέπει την πολυνηματική μεταγλώττιση Shader σε χρόνο εκτέλεσης, μειώνει το τρεμόπαιγμα και βελτιώνει την απόδοση των προγραμμάτων οδήγησης χωρίς τη δική τους υποστήριξη πολλαπλών νημάτων. Ποικίλες κορυφαίες επιδόσεις σε προγράμματα οδήγησης με multithreading. Μπορεί να χρειαστεί επανεκκίνηση του Ryujinx για να απενεργοποιήσετε σωστά την ενσωματωμένη λειτουργία πολλαπλών νημάτων του προγράμματος οδήγησης ή ίσως χρειαστεί να το κάνετε χειροκίνητα για να έχετε την καλύτερη απόδοση.",
"en_US": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.",
"es_ES": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.",
@@ -13929,7 +13529,7 @@
"ID": "NetLogTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Druckt Netzwerk-Logmeldungen in die Konsole.",
"el_GR": "",
"en_US": "Prints network log messages in the console.",
"es_ES": "Imprimir registros de red en la consola.",
@@ -13979,7 +13579,7 @@
"ID": "AvaloniaLogTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Druckt Avalonia (UI) Logmeldungen in die Konsole.",
"el_GR": "",
"en_US": "Prints Avalonia (UI) log messages in the console.",
"es_ES": "Muestra los mensajes del registro de Avalonia (UI) en la consola.",
@@ -14554,7 +14154,7 @@
"ID": "UpdaterBackgroundStatusBarButtonText",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Update verfügbar!",
"el_GR": "",
"en_US": "Update Available!",
"es_ES": "Actualizacion Disponible!",
@@ -14854,7 +14454,7 @@
"ID": "TitleBundledUpdateVersionLabel",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Gebündelt: Version {0}",
"el_GR": "",
"en_US": "Bundled: Version {0}",
"es_ES": "Incorporado: Versión {0}",
@@ -14879,7 +14479,7 @@
"ID": "TitleBundledDlcLabel",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Gebündelt:",
"el_GR": "",
"en_US": "Bundled:",
"es_ES": "Incorporado:",
@@ -14900,156 +14500,6 @@
"zh_TW": "附帶:"
}
},
- {
- "ID": "TitleXCIStatusPartialLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Partial",
- "es_ES": "Parcial",
- "fr_FR": "Partiel",
- "he_IL": "",
- "it_IT": "Parziale",
- "ja_JP": "",
- "ko_KR": "일부",
- "no_NO": "Delvis",
- "pl_PL": "",
- "pt_BR": "Parcial",
- "ru_RU": "Частично",
- "sv_SE": "Delvis",
- "th_TH": "ยังไม่สมบูรณ์",
- "tr_TR": "",
- "uk_UA": "Часткові",
- "zh_CN": "分区",
- "zh_TW": "部分"
- }
- },
- {
- "ID": "TitleXCIStatusTrimmableLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Untrimmed",
- "es_ES": "Sin Recortar",
- "fr_FR": "Non Réduit",
- "he_IL": "",
- "it_IT": "Dim. originale",
- "ja_JP": "",
- "ko_KR": "트리밍되지 않음",
- "no_NO": "Ikke trimmet",
- "pl_PL": "",
- "pt_BR": "Não Reduzido",
- "ru_RU": "Не обрезан",
- "sv_SE": "Orörd",
- "th_TH": "ยังไม่ได้ตัดแต่ง",
- "tr_TR": "",
- "uk_UA": "Необрізані",
- "zh_CN": "没有瘦身的",
- "zh_TW": "未修剪"
- }
- },
- {
- "ID": "TitleXCIStatusUntrimmableLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Trimmed",
- "es_ES": "Recortado",
- "fr_FR": "Réduit",
- "he_IL": "",
- "it_IT": "Dim. ridotta",
- "ja_JP": "",
- "ko_KR": "트리밍됨",
- "no_NO": "Trimmet",
- "pl_PL": "",
- "pt_BR": "Reduzido",
- "ru_RU": "Обрезан",
- "sv_SE": "Optimerad",
- "th_TH": "ตัดแต่งแล้ว",
- "tr_TR": "",
- "uk_UA": "Обрізані",
- "zh_CN": "经过瘦身的",
- "zh_TW": "已修剪"
- }
- },
- {
- "ID": "TitleXCIStatusFailedLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "(Failed)",
- "es_ES": "(Fallido)",
- "fr_FR": "(Échoué)",
- "he_IL": "",
- "it_IT": "(Fallito)",
- "ja_JP": "",
- "ko_KR": "(실패)",
- "no_NO": "(Mislyktes)",
- "pl_PL": "",
- "pt_BR": "Falhou",
- "ru_RU": "(Ошибка)",
- "sv_SE": "(misslyckades)",
- "th_TH": "(ล้มเหลว)",
- "tr_TR": "",
- "uk_UA": "(Невдача)",
- "zh_CN": "(失败)",
- "zh_TW": "(失敗)"
- }
- },
- {
- "ID": "TitleXCICanSaveLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Save {0:n0} Mb",
- "es_ES": "Ahorra {0:n0} Mb",
- "fr_FR": "Sauvegarde de {0:n0} Mo",
- "he_IL": "",
- "it_IT": "Risparmia {0:n0} MB",
- "ja_JP": "",
- "ko_KR": "{0:n0}MB 저장",
- "no_NO": "Spare {0:n0} Mb",
- "pl_PL": "",
- "pt_BR": "Salvar {0:n0} Mb",
- "ru_RU": "Освободить {0:n0} Мб",
- "sv_SE": "Spara {0:n0} Mb",
- "th_TH": "บันทึก {0:n0} เมกะไบต์",
- "tr_TR": "",
- "uk_UA": "Зберегти {0:n0} Мб",
- "zh_CN": "能节约 {0:n0} Mb",
- "zh_TW": "可節省 {0:n0} Mb"
- }
- },
- {
- "ID": "TitleXCISavingLabel",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Saved {0:n0} Mb",
- "es_ES": "{0:n0} Mb ahorrado(s)",
- "fr_FR": "Sauvegardé {0:n0} Mo",
- "he_IL": "",
- "it_IT": "Risparmiati {0:n0} MB",
- "ja_JP": "",
- "ko_KR": "{0:n0}MB 저장됨",
- "no_NO": "Spart {0:n0} Mb",
- "pl_PL": "",
- "pt_BR": "Salvo {0:n0} Mb",
- "ru_RU": "Освобождено {0:n0} Мб",
- "sv_SE": "Sparade {0:n0} Mb",
- "th_TH": "บันทึกแล้ว {0:n0} เมกะไบต์",
- "tr_TR": "",
- "uk_UA": "Збережено {0:n0} Мб",
- "zh_CN": "节约了 {0:n0} Mb",
- "zh_TW": "已節省 {0:n0} Mb"
- }
- },
{
"ID": "RyujinxInfo",
"Translations": {
@@ -15200,81 +14650,6 @@
"zh_TW": "長度必須為 {0} 到 {1} 個字元"
}
},
- {
- "ID": "CabinetTitle",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Cabinet Dialog",
- "es_ES": "Diálogo Gabinete",
- "fr_FR": "Dialogue de Cabinet",
- "he_IL": "",
- "it_IT": "Modale del Cabinet",
- "ja_JP": "",
- "ko_KR": "캐비닛 대화 상자",
- "no_NO": "Dialogboks for kabinett",
- "pl_PL": "",
- "pt_BR": "Diálogo do Gabinete",
- "ru_RU": "Сообщение кабинета",
- "sv_SE": "Cabinet-dialog",
- "th_TH": "กล่องโต้ตอบไฟล์ Cabinet",
- "tr_TR": "",
- "uk_UA": "Сповіщення Cabinet",
- "zh_CN": "档案对话框",
- "zh_TW": "Cabinet 對話方塊"
- }
- },
- {
- "ID": "CabinetDialog",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Enter your Amiibo's new name",
- "es_ES": "Ingresa el nuevo nombre de tu Amiibo",
- "fr_FR": "Entrer le nouveau nom de votre Amiibo",
- "he_IL": "",
- "it_IT": "Inserisci il nuovo nome del tuo Amiibo",
- "ja_JP": "",
- "ko_KR": "Amiibo의 새 이름 입력하기",
- "no_NO": "Skriv inn Amiiboens nye navn",
- "pl_PL": "",
- "pt_BR": "Digite o novo nome do seu Amiibo",
- "ru_RU": "Введите новое имя вашего Amiibo",
- "sv_SE": "Ange nya namnet för din Amiibo",
- "th_TH": "กรุณาใส่ชื่อใหม่ของ Amiibo ของคุณ",
- "tr_TR": "",
- "uk_UA": "Вкажіть Ваше нове ім'я Amiibo",
- "zh_CN": "输入你的 Amiibo 的新名字",
- "zh_TW": "輸入 Amiibo 的新名稱"
- }
- },
- {
- "ID": "CabinetScanDialog",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Please scan your Amiibo now.",
- "es_ES": "Escanea tu Amiibo ahora.",
- "fr_FR": "Veuillez scannez votre Amiibo.",
- "he_IL": "",
- "it_IT": "Scansiona ora il tuo Amiibo.",
- "ja_JP": "",
- "ko_KR": "지금 Amiibo를 스캔하세요.",
- "no_NO": "Vennligst skann Amiiboene dine nå.",
- "pl_PL": "",
- "pt_BR": "Por favor, escaneie seu Amiibo agora.",
- "ru_RU": "Пожалуйста, сканируйте ваш Amiibo.",
- "sv_SE": "Skanna din Amiibo nu.",
- "th_TH": "กรุณาสแกน Amiibo ของคุณตอนนี้",
- "tr_TR": "",
- "uk_UA": "Будь ласка, проскануйте Ваш Amiibo.",
- "zh_CN": "请现在扫描你的 Amiibo",
- "zh_TW": "請掃描你的 Amiibo。"
- }
- },
{
"ID": "SoftwareKeyboard",
"Translations": {
@@ -15800,56 +15175,6 @@
"zh_TW": "正在編譯著色器"
}
},
- {
- "ID": "OpenFileDialogTitle",
- "Translations": {
- "ar_SA": "حدد ملف مدعوم لفتحه",
- "de_DE": "Wähle eine unterstützte Datei",
- "el_GR": "Επιλέξτε ένα υποστηριζόμενο αρχείο για άνοιγμα",
- "en_US": "Select a supported file to open",
- "es_ES": "Selecciona un archivo soportado para cargar",
- "fr_FR": "Sélectionnez un fichier compatible à ouvrir",
- "he_IL": "בחר קובץ נתמך לפתיחה",
- "it_IT": "Seleziona un file supportato da aprire",
- "ja_JP": "開くファイルを選択",
- "ko_KR": "지원되는 파일을 선택하여 열기",
- "no_NO": "Velg en støttet fil for å åpne",
- "pl_PL": "Wybierz obsługiwany plik do otwarcia",
- "pt_BR": "Selecione um arquivo suportado para abrir",
- "ru_RU": "Выберите совместимый файл для открытия",
- "sv_SE": "Välj en fil som stöds att öppna",
- "th_TH": "เลือกไฟล์ที่สนับสนุนเพื่อเปิด",
- "tr_TR": "Açmak için desteklenen bir dosya seçin",
- "uk_UA": "Виберіть підтримуваний файл для відкриття",
- "zh_CN": "选择支持的游戏文件并加载",
- "zh_TW": "選取支援的檔案格式"
- }
- },
- {
- "ID": "AllSupportedFormats",
- "Translations": {
- "ar_SA": "كل التنسيقات المدعومة",
- "de_DE": "Alle unterstützten Formate",
- "el_GR": "Όλες Οι Υποστηριζόμενες Μορφές",
- "en_US": "All Supported Formats",
- "es_ES": "Todos los Formatos Soportados",
- "fr_FR": "Tous les Formats Supportés",
- "he_IL": "כל הפורמטים הנתמכים",
- "it_IT": "Tutti i formati supportati",
- "ja_JP": "すべての対応フォーマット",
- "ko_KR": "지원되는 모든 형식",
- "no_NO": "Alle støttede formater",
- "pl_PL": "Wszystkie Obsługiwane Formaty",
- "pt_BR": "Todos os Formatos Suportados",
- "ru_RU": "Все поддерживаемые форматы",
- "sv_SE": "Alla format som stöds",
- "th_TH": "รูปแบบที่รองรับทั้งหมด",
- "tr_TR": "Tüm Desteklenen Formatlar",
- "uk_UA": "Усі підтримувані формати",
- "zh_CN": "所有支持的格式",
- "zh_TW": "所有支援的格式"
- }
- },
{
"ID": "RyujinxUpdater",
"Translations": {
@@ -15954,7 +15279,7 @@
"ID": "SettingsTabHotkeysScreenshotHotkey",
"Translations": {
"ar_SA": "لقطة الشاشة:",
- "de_DE": "",
+ "de_DE": "Bildschirmfoto:",
"el_GR": "Στιγμιότυπο Οθόνης:",
"en_US": "Screenshot:",
"es_ES": "Captura de Pantalla:",
@@ -16104,7 +15429,7 @@
"ID": "ControllerLedTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "LED-Einstellungen",
"el_GR": "",
"en_US": "LED Settings",
"es_ES": "Configuración LED",
@@ -16200,106 +15525,6 @@
"zh_TW": "管理帳戶 - 大頭貼"
}
},
- {
- "ID": "Amiibo",
- "Translations": {
- "ar_SA": "أميبو",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Amiibo",
- "es_ES": null,
- "fr_FR": null,
- "he_IL": "אמיבו",
- "it_IT": "",
- "ja_JP": "",
- "ko_KR": null,
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": null,
- "ru_RU": null,
- "sv_SE": null,
- "th_TH": "อมิโบ้",
- "tr_TR": "",
- "uk_UA": "",
- "zh_CN": null,
- "zh_TW": null
- }
- },
- {
- "ID": "Unknown",
- "Translations": {
- "ar_SA": "غير معروف",
- "de_DE": "Unbekannt",
- "el_GR": "Άγνωστο",
- "en_US": "Unknown",
- "es_ES": "Desconocido",
- "fr_FR": "Inconnu",
- "he_IL": "לא ידוע",
- "it_IT": "Sconosciuto",
- "ja_JP": "不明",
- "ko_KR": "알 수 없음",
- "no_NO": "Ukjent",
- "pl_PL": "Nieznane",
- "pt_BR": "Desconhecido",
- "ru_RU": "Неизвестно",
- "sv_SE": "Okänt",
- "th_TH": "ไม่รู้จัก",
- "tr_TR": "Bilinmeyen",
- "uk_UA": "Невідомо",
- "zh_CN": "未知",
- "zh_TW": "未知"
- }
- },
- {
- "ID": "Usage",
- "Translations": {
- "ar_SA": "الاستخدام",
- "de_DE": "Nutzung",
- "el_GR": "Χρήση",
- "en_US": "Usage",
- "es_ES": "Uso",
- "fr_FR": "Utilisation",
- "he_IL": "שימוש",
- "it_IT": "Utilizzo",
- "ja_JP": "使用法",
- "ko_KR": "사용법",
- "no_NO": "Forbruk",
- "pl_PL": "Użycie",
- "pt_BR": "Uso",
- "ru_RU": "Применение",
- "sv_SE": "Användning",
- "th_TH": "การใช้งาน",
- "tr_TR": "Kullanım",
- "uk_UA": "Використання",
- "zh_CN": "用法",
- "zh_TW": "用途"
- }
- },
- {
- "ID": "Writable",
- "Translations": {
- "ar_SA": "قابل للكتابة",
- "de_DE": "Beschreibbar",
- "el_GR": "Εγγράψιμο",
- "en_US": "Writable",
- "es_ES": "Escribible",
- "fr_FR": "Ecriture possible",
- "he_IL": "ניתן לכתיבה",
- "it_IT": "Scrivibile",
- "ja_JP": "書き込み可能",
- "ko_KR": "쓰기 가능",
- "no_NO": "Skrivbart",
- "pl_PL": "Zapisywalne",
- "pt_BR": "Gravável",
- "ru_RU": "Доступно для записи",
- "sv_SE": "Skrivbar",
- "th_TH": "สามารถเขียนทับได้",
- "tr_TR": "Yazılabilir",
- "uk_UA": "Можливість запису",
- "zh_CN": "可写入",
- "zh_TW": "可寫入"
- }
- },
{
"ID": "SelectDlcDialogTitle",
"Translations": {
@@ -16375,306 +15600,6 @@
"zh_TW": "選取模組資料夾"
}
},
- {
- "ID": "TrimXCIFileDialogPrimaryText",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "This function will first check the empty space and then trim the XCI File to save disk space.",
- "es_ES": "Esta función verificará el espacio vacío y después recortará el archivo XCI para ahorrar espacio en disco",
- "fr_FR": "Cette fonction vérifie d’abord l’espace libre, puis réduit le fichier XCI pour économiser de l’espace disque.",
- "he_IL": "",
- "it_IT": "Questa funzionalità controllerà prima lo spazio libero e poi ridurrà la dimensione del file XCI per risparmiare spazio su disco.",
- "ja_JP": "",
- "ko_KR": "이 기능은 먼저 충분한 공간을 확보한 다음 XCI 파일을 트리밍하여 디스크 공간을 절약합니다.",
- "no_NO": "Denne funksjonen kontrollerer først hvor mye plass som er ledig, og trimmer deretter XCI-filen for å spare diskplass.",
- "pl_PL": "",
- "pt_BR": "Esta função primeiro verificará o espaço vazio e depois reduzirá o arquivo XCI para economizar espaço em disco.",
- "ru_RU": "Эта функция сначала проверит наличие пустого пространства, а затем обрежет XCI файл для экономии места на диске.",
- "sv_SE": "Denna funktion kommer först att kontrollera ledigt utrymme och sedan optimera XCI-filen för att spara diskutrymme.",
- "th_TH": "ฟังก์ชันนี้จะตรวจสอบพื้นที่ว่างก่อน จากนั้นจึงตัดแต่งไฟล์ XCI เพื่อประหยัดพื้นที่ดิสก์",
- "tr_TR": "",
- "uk_UA": "Ця функція спочатку перевірить наявність порожнього місця, після чого обріже файл XCI для економії місця на диску.",
- "zh_CN": "这个功能将会先检查 XCI 文件,再对其执行瘦身操作以节约磁盘空间。",
- "zh_TW": "此功能首先檢查 XCI 檔案是否有可修剪的字元,然後修剪檔案以節省儲存空間。"
- }
- },
- {
- "ID": "TrimXCIFileDialogSecondaryText",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB",
- "es_ES": "Tamaño de archivo actual: {0:n} MB\nTamaño de datos de juego: {1:n} MB\nAhorro de espacio en disco: {2:n} MB",
- "fr_FR": "Taille actuelle du fichier: {0:n} MB\nTaille des données du jeu: {1:n} MB\nEspace disque économisé: {2:n} MB",
- "he_IL": "",
- "it_IT": "Dimensione attuale del file: {0:n} MB\nDimensione dei dati del gioco: {1:n} MB\nRisparmio spazio su disco: {2:n} MB",
- "ja_JP": "",
- "ko_KR": "현재 파일 크기 : {0:n}MB\n게임 데이터 크기 : {1:n}MB\n디스크 공간 절약 : {2:n}MB",
- "no_NO": "Nåværende filstørrelse: 0:n MB\nSpilldatastørrelse: {1:n} MB\nDiskplassbesparelse: {2:n} MB",
- "pl_PL": "",
- "pt_BR": "Tamanho atual do arquivo: {0:n} MB\nTamanho dos dados do jogo: {1:n} MB\nEconomia de espaço em disco: {2:n} MB",
- "ru_RU": "Размер текущего файла: {0:n} Мб\nРазмер игровых данных: {1:n} MB\nЭкономия дискового пространства: {2:n} Мб",
- "sv_SE": "Aktuell filstorlek: {0:n} MB\nStorlek för speldata: {1:n} MB\nSparat diskutrymme: {2:n} MB",
- "th_TH": "ขนาดไฟล์ปัจจุบัน: {0:n} เมกะไบต์\nขนาดข้อมูลเกม: {1:n} เมกะไบต์\nพื้นที่ดิสก์ที่ประหยัดได้: {2:n} เมกะไบต์",
- "tr_TR": "",
- "uk_UA": "Поточний розмір файла: {0:n} MB\nРозмір файлів гри: {1:n} MB\nЕкономія місця: {2:n} MB",
- "zh_CN": "当前文件大小: {0:n} MB\n游戏数据大小: {1:n} MB\n节约的磁盘空间: {2:n} MB",
- "zh_TW": "現在的檔案大小: {0:n} MB\n遊戲資料大小: {1:n} MB\n節省的儲存空間: {2:n} MB"
- }
- },
- {
- "ID": "TrimXCIFileNoTrimNecessary",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File does not need to be trimmed. Check logs for further details",
- "es_ES": "El archivo XCI no necesita ser recortado. Verifica los logs para más detalles.",
- "fr_FR": "Le Fichier XCI n’a pas besoin d’être réduit. Référez-vous aux journaux pour plus de détails.",
- "he_IL": "",
- "it_IT": "Non è necessario ridurre la dimensione del file XCI. Controlla i log per ulteriori dettagli",
- "ja_JP": "",
- "ko_KR": "XCI 파일은 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인",
- "no_NO": "XCI-filen trenger ikke å trimmes. Sjekk loggene for mer informasjon",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI não precisa ser reduzido. Verifique os logs para mais detalhes",
- "ru_RU": "XCI файл содержит недопустимые данные. Проверьте логи для получения дополнительной информации.",
- "sv_SE": "XCI-filen behöver inte optimeras. Kontrollera loggen för mer information",
- "th_TH": "ไฟล์ XCI ไม่จำเป็นต้องถูกตัดแต่ง โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม",
- "tr_TR": "",
- "uk_UA": "XCI файл не потребує обрізання. Перевірте журнали (logs) для отримання додаткової інформації",
- "zh_CN": "XCI 文件不需要被瘦身。查看日志以获得更多细节。",
- "zh_TW": "XCI 檔案不需要修剪。檢查日誌以取得更多資訊"
- }
- },
- {
- "ID": "TrimXCIFileNoUntrimPossible",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File cannot be untrimmed. Check logs for further details",
- "es_ES": "El recorte del archivo XCI no puede ser deshecho. Verifica los registros para más detalles.",
- "fr_FR": "Fichier XCI ne peut pas être dé-réduit. Référez-vous aux journaux pour plus de détails",
- "he_IL": "",
- "it_IT": "Il file XCI non può essere riportato alla sua dimensione originale. Controlla i log per ulteriori dettagli",
- "ja_JP": "",
- "ko_KR": "XCI 파일은 트리밍을 해제할 수 없습니다. 자세한 내용은 로그를 확인",
- "no_NO": "XCI-filen kan ikke trimmes. Sjekk loggene for mer informasjon",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI reduzido não pode ser desfeito. Verifique os logs para mais detalhes",
- "ru_RU": "XCI файл не может быть обрезан. Проверьте логи для получения более подробной информации",
- "sv_SE": "XCI-filen kan inte avoptimeras. Kontrollera loggen för mer information",
- "th_TH": "ไฟล์ XCI ไม่สามารถคืนสภาพเดิม (untrimmed) ได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม",
- "tr_TR": "",
- "uk_UA": "XCI файл не може бути обрізаний. Перевірте журнали (logs) для отримання додаткової інформації",
- "zh_CN": "XCI 文件不能被瘦身。查看日志以获得更多细节。",
- "zh_TW": "XCI 檔案不能被修剪。檢查日誌以取得更多資訊"
- }
- },
- {
- "ID": "TrimXCIFileReadOnlyFileCannotFix",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File is Read Only and could not be made writable. Check logs for further details",
- "es_ES": "El archivo XCI es de solo Lectura y no se le puede escribir. Lee el registro para más información.",
- "fr_FR": "Fichier XCI est en Lecture Seule et n'a pas pu être rendu accessible en écriture. Référez-vous aux journaux pour plus de détails",
- "he_IL": "",
- "it_IT": "Il file XCI è in sola lettura e non può essere reso accessibile in scrittura. Controlla i log per ulteriori dettagli",
- "ja_JP": "",
- "ko_KR": "XCI 파일은 읽기 전용이므로 쓰기 가능하게 만들 수 없습니다. 자세한 내용은 로그를 확인",
- "no_NO": "XCI-filen er skrivebeskyttet og kunne ikke gjøres skrivbar. Sjekk loggene for mer informasjon",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI é somente leitura e não pôde ser tornado gravável. Verifique os logs para mais detalhes",
- "ru_RU": "XCI файл доступен только для чтения и его невозможно сделать доступным для записи. Проверьте логи для получения более подробной информации",
- "sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information",
- "th_TH": "ไฟล์ XCI เป็นแบบอ่านอย่างเดียว และไม่สามารถเปลี่ยนเป็นแบบเขียนได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม",
- "tr_TR": "",
- "uk_UA": "XCI файл \"Тільки для Читання\" і не може бути прочитаним. Перевірте журнали (logs) для отримання додаткової інформації",
- "zh_CN": "XCI 文件是只读的,且不可以被标记为可读取的。查看日志以获得更多细节。",
- "zh_TW": "XCI 檔案是唯讀,並且無法改成可寫入。檢查日誌以取得更多資訊"
- }
- },
- {
- "ID": "TrimXCIFileFileSizeChanged",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.",
- "es_ES": "El archivo XCI ha cambiado de tamaño desde que fue escaneado. Verifica que no se esté escribiendo al archivo y vuelve a intentarlo.",
- "fr_FR": "La taille du Fichier XCI a changé depuis son analyse. Veuillez vérifier que le fichier n’est pas en cours d’écriture, puis réessayez.",
- "he_IL": "",
- "it_IT": "La dimensione del file XCI è cambiata da quando è stato scansionato. Controlla che il file non stia venendo scritto da qualche altro programma e poi riprova.",
- "ja_JP": "",
- "ko_KR": "XCI 파일이 스캔된 후 크기가 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.",
- "no_NO": "XCI File har endret størrelse siden den ble skannet. Kontroller at det ikke skrives til filen, og prøv på nytt.",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI mudou de tamanho desde que foi escaneado. Verifique se o arquivo não está sendo gravado e tente novamente.",
- "ru_RU": "XCI файл изменился в размере после сканирования. Проверьте, не производится ли запись в этот файл, и повторите попытку.",
- "sv_SE": "XCI-filen har ändrats i storlek sedan den lästes av. Kontrollera att filen inte skrivs till och försök igen.",
- "th_TH": "ขนาดไฟล์ XCI มีการเปลี่ยนแปลงตั้งแต่ที่สแกนครั้งล่าสุด กรุณาตรวจสอบว่าไฟล์ไม่ได้ถูกเขียนข้อมูล และลองใหม่อีกครั้ง",
- "tr_TR": "",
- "uk_UA": "Розмір файлу XCI змінився з моменту сканування. Перевірте, чи не записується файл, та спробуйте знову",
- "zh_CN": "XCI 文件在扫描后大小发生了变化。请检查文件是否未被写入,然后重试。",
- "zh_TW": "XCI 檔案大小比較上次的掃瞄已經改變。請檢查檔案是否未被寫入,然後再嘗試。"
- }
- },
- {
- "ID": "TrimXCIFileFreeSpaceCheckFailed",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File has data in the free space area, it is not safe to trim",
- "es_ES": "El archivo XCI tiene datos en el área de espacio libre, no es seguro recortar.",
- "fr_FR": "Fichier XCI a des données dans la zone d'espace libre, ce n'est pas sûr de réduire",
- "he_IL": "",
- "it_IT": "Il file XCI contiene dei dati nello spazio libero, non è sicuro ridurne la dimensione",
- "ja_JP": "",
- "ko_KR": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않음",
- "no_NO": "XCI-filen har data i ledig plass, og det er ikke trygt å trimme den",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI tem dados na área de espaço livre, não é seguro reduzi-lo",
- "ru_RU": "XCI файл содержит данные в пустой зоне, обрезать его небезопасно",
- "sv_SE": "XCI-filen har data i det lediga utrymmet. Den är inte säker att optimera",
- "th_TH": "ไฟล์ XCI มีข้อมูลในพื้นที่ว่าง จึงไม่ปลอดภัยที่จะทำการตัดแต่ง",
- "tr_TR": "",
- "uk_UA": "Файл XCI містить дані в зоні вільного простору, тому обрізка небезпечна",
- "zh_CN": "XCI 文件的空闲区域内有数据,不能安全瘦身。",
- "zh_TW": "XCI 檔案有數據儲存於可節省儲存空間的區域,所以試圖修剪並不安全"
- }
- },
- {
- "ID": "TrimXCIFileInvalidXCIFile",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File contains invalid data. Check logs for further details",
- "es_ES": "El archivo XCI contiene datos inválidos. Lee el registro para más información.",
- "fr_FR": "Fichier XCI contient des données invalides. Regarder les journaux pour plus de détails",
- "he_IL": "",
- "it_IT": "Il file XCI contiene dei dati non validi. Controlla i log per ulteriori dettagli",
- "ja_JP": "",
- "ko_KR": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 자세한 내용은 로그를 확인",
- "no_NO": "XCI-filen inneholder ugyldige data. Sjekk loggene for ytterligere detaljer",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI contém dados inválidos. Verifique os logs para obter mais detalhes",
- "ru_RU": "XCI файл содержит недопустимые данные. Проверьте логи для получения дополнительной информации",
- "sv_SE": "XCI-filen innehåller ogiltig data. Kontrollera loggen för mer information",
- "th_TH": "ไฟล์ XCI มีข้อมูลที่ไม่ถูกต้อง โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม",
- "tr_TR": "",
- "uk_UA": "XCI Файл містить недійсні дані. Перевірте журнали (logs) для отримання додаткової інформації",
- "zh_CN": "XCI 文件含有无效数据。查看日志以获得更多细节。",
- "zh_TW": "XCI 檔案帶有無效的數據。檢查日誌以取得更多資訊"
- }
- },
- {
- "ID": "TrimXCIFileFileIOWriteError",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "XCI File could not be opened for writing. Check logs for further details",
- "es_ES": "El archivo XCI no se puede abrir para escribirlo. Lee el registro para más información.",
- "fr_FR": "Fichier XCI n'a pas pu être ouvert pour écriture. Regarder les journaux pour plus de détails",
- "he_IL": "",
- "it_IT": "Il file XCI non può essere aperto in scrittura. Controlla i log per ulteriori dettagli",
- "ja_JP": "",
- "ko_KR": "XCI 파일을 쓰기 위해 열 수 없습니다. 자세한 내용은 로그를 확인",
- "no_NO": "XCI-filen kunne ikke åpnes for skriving. Sjekk loggene for ytterligere detaljer",
- "pl_PL": "",
- "pt_BR": "O arquivo XCI não pôde ser aberto para gravação. Verifique os logs para mais detalhes",
- "ru_RU": "Не удалось открыть XCI файл для записи. Проверьте журналы для получения дополнительной информации",
- "sv_SE": "XCI-filen kunde inte öppnas för skrivning. Kontrollera loggen för mer information",
- "th_TH": "ไม่สามารถเปิดไฟล์ XCI เพื่อเขียนข้อมูลได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม",
- "tr_TR": "",
- "uk_UA": "XCI Файл файл не вдалося відкрити для запису. Перевірте журнали для додаткової інформації",
- "zh_CN": "XCI 文件不能被读写。查看日志以获得更多细节。",
- "zh_TW": "XCI 檔案不能被寫入。檢查日誌以取得更多資訊"
- }
- },
- {
- "ID": "TrimXCIFileFailedPrimaryText",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Trimming of the XCI file failed",
- "es_ES": "El recorte del archivo XCI falló",
- "fr_FR": "Réduction du fichier XCI a échoué",
- "he_IL": "",
- "it_IT": "Riduzione della dimensione del file XCI fallita",
- "ja_JP": "",
- "ko_KR": "XCI 파일 트리밍에 실패",
- "no_NO": "Trimming av XCI-filen mislyktes",
- "pl_PL": "",
- "pt_BR": "A redução do arquivo XCI falhou",
- "ru_RU": "Обрезка XCI файла не удалась",
- "sv_SE": "Optimering av XCI-filen misslyckades",
- "th_TH": "การตัดแต่งไฟล์ XCI ล้มเหลว",
- "tr_TR": "",
- "uk_UA": "Не вдалося обрізати файл XCI",
- "zh_CN": "XCI 文件瘦身失败",
- "zh_TW": "修剪 XCI 檔案失敗"
- }
- },
- {
- "ID": "TrimXCIFileCancelled",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "The operation was cancelled",
- "es_ES": "La operación fue cancelada",
- "fr_FR": "L'opération a été annulée",
- "he_IL": "",
- "it_IT": "L'operazione è stata annullata",
- "ja_JP": "",
- "ko_KR": "작업이 취소됨",
- "no_NO": "Operasjonen ble avlyst",
- "pl_PL": "",
- "pt_BR": "A operação foi cancelada",
- "ru_RU": "Операция была отменена",
- "sv_SE": "Åtgärden avbröts",
- "th_TH": "การดำเนินการถูกยกเลิกแล้ว",
- "tr_TR": "",
- "uk_UA": "Операція перервана",
- "zh_CN": "操作已取消",
- "zh_TW": "修剪已取消"
- }
- },
- {
- "ID": "TrimXCIFileFileUndertermined",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "No operation was performed",
- "es_ES": "No se realizó ninguna operación",
- "fr_FR": "Aucune opération a été faite",
- "he_IL": "",
- "it_IT": "Non è stata effettuata alcuna operazione",
- "ja_JP": "",
- "ko_KR": "작업이 수행되지 않음",
- "no_NO": "Ingen operasjon ble utført",
- "pl_PL": "",
- "pt_BR": "Nenhuma operação foi realizada",
- "ru_RU": "Операция не была проведена",
- "sv_SE": "Ingen åtgärd genomfördes",
- "th_TH": "ไม่มีการดำเนินการใด ๆ ถูกทำขึ้น",
- "tr_TR": "",
- "uk_UA": "Операція не проводилася",
- "zh_CN": "未执行操作",
- "zh_TW": "沒有修剪"
- }
- },
{
"ID": "UserProfileWindowTitle",
"Translations": {
@@ -16754,7 +15679,7 @@
"ID": "ModWindowTitle",
"Translations": {
"ar_SA": "إدارة التعديلات لـ {0} ({1})",
- "de_DE": "",
+ "de_DE": "Mods verwalten für {0} ({1})",
"el_GR": "",
"en_US": "Manage Mods for {0} ({1})",
"es_ES": "Administrar Mods para {0} ({1})",
@@ -16801,385 +15726,35 @@
}
},
{
- "ID": "XCITrimmerWindowTitle",
+ "ID": "UpdateWindowUpdateAddedMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "{0} Update(s) hinzugefügt",
"el_GR": "",
- "en_US": "XCI File Trimmer",
- "es_ES": "Recortador de Archivos XCI",
- "fr_FR": "Réducteur de Fichiers XCI",
+ "en_US": "{0} new update(s) added",
+ "es_ES": "{0} nueva(s) actualización(es) agregada(s)",
+ "fr_FR": "{0} nouvelle(s) mise(s) à jour ajoutée(s)",
"he_IL": "",
- "it_IT": "Riduci dimensioni dei file XCI",
+ "it_IT": "{0} nuovo/i aggiornamento/i aggiunto/i",
"ja_JP": "",
- "ko_KR": "XCI 파일 트리머",
- "no_NO": "",
+ "ko_KR": "{0}개의 신규 업데이트가 추가됨",
+ "no_NO": "{0} ny(e) oppdatering(er) lagt til",
"pl_PL": "",
- "pt_BR": "Redutor de Arquivo XCI",
- "ru_RU": "Триммер XCI файлов",
- "sv_SE": "Optimera XCI-filer",
- "th_TH": "เครื่องมือตัดแต่งไฟล์ XCI",
+ "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)",
+ "ru_RU": "Добавлено {0} новых обновлений",
+ "sv_SE": "{0} nya uppdatering(ar) lades till",
+ "th_TH": "มีอัปเดตใหม่เข้ามา {0} รายการ",
"tr_TR": "",
- "uk_UA": "Обрізка XCI Файлів",
- "zh_CN": "XCI 文件瘦身器",
- "zh_TW": "XCI 檔案修剪器"
- }
- },
- {
- "ID": "XCITrimmerTitleStatusCount",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "{0} of {1} Title(s) Selected",
- "es_ES": "{0} de {1} Título(s) Seleccionado(s)",
- "fr_FR": "{0} sur {1} Fichier(s) Sélectionnés",
- "he_IL": "",
- "it_IT": "{0} di {1} titoli selezionati",
- "ja_JP": "",
- "ko_KR": "{1}개 타이틀 중 {0}개 선택됨",
- "no_NO": "{0} av {1} Valgte tittel(er)",
- "pl_PL": "",
- "pt_BR": "{0} de {1} Título(s) Selecionado(s)",
- "ru_RU": "{0} из {1} файл(а/ов) выбран(о)",
- "sv_SE": "{0} av {1} spel markerade",
- "th_TH": "เลือก {0} จาก {1} รายการ",
- "tr_TR": "",
- "uk_UA": "{0} з {1} тайтл(ів) обрано",
- "zh_CN": "在 {1} 中选中了 {0} 个游戏 ",
- "zh_TW": "已選擇 {1} 之 {0} 的遊戲"
- }
- },
- {
- "ID": "XCITrimmerTitleStatusCountWithFilter",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "{0} of {1} Title(s) Selected ({2} displayed)",
- "es_ES": "{0} de {1} Título(s) Seleccionado(s) ({2} Mostrado(s))",
- "fr_FR": "{0} sur {1} Fichier(s) Sélectionnés ({2} affiché(s)",
- "he_IL": "",
- "it_IT": "{0} di {1} titoli selezionati ({2} visualizzati)",
- "ja_JP": "",
- "ko_KR": "{1}개 타이틀 중 {0}개 선택됨({2}개 표시됨)",
- "no_NO": "{0} av {1} Tittel(er) valgt ({2} vises)",
- "pl_PL": "",
- "pt_BR": "{0} de {1} Título(s) Selecionado(s) ({2} exibidos)",
- "ru_RU": "{0} из {1} файл(а/ов) выбран(о) ({2} показан(о))",
- "sv_SE": "{0} av {1} spel markerade ({2} visade)",
- "th_TH": "เลือก {0} จาก {1} รายการ (แสดงผล {2} รายการ)",
- "tr_TR": "",
- "uk_UA": "{0} з {1} тайтл(ів) обрано ({2} відображається)",
- "zh_CN": "在 {1} 中选中了 {0} 个游戏 (显示了 {2} 个)",
- "zh_TW": "已選擇 {1} 之 {0} 的遊戲 (已顯示 {2} 個)"
- }
- },
- {
- "ID": "XCITrimmerTitleStatusTrimming",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Trimming {0} Title(s)...",
- "es_ES": "Recortando {0} Título(s)...",
- "fr_FR": "Réduction de {0} Fichier(s)...",
- "he_IL": "",
- "it_IT": "Riduzione delle dimensioni di {0} titolo/i...",
- "ja_JP": "",
- "ko_KR": "{0}개의 타이틀을 트리밍 중...",
- "no_NO": "Trimming av {0} tittel(er)...",
- "pl_PL": "",
- "pt_BR": "Reduzindo {0} Título(s)...",
- "ru_RU": "Обрезка {0} файла(ов)...",
- "sv_SE": "Optimerar {0} spel...",
- "th_TH": "กำลังตัดแต่ง {0} รายการ...",
- "tr_TR": "",
- "uk_UA": "Обрізка {0} тайтл(ів)...",
- "zh_CN": "{0} 个游戏瘦身中。。。",
- "zh_TW": "正在修剪 {0} 個遊戲..."
- }
- },
- {
- "ID": "XCITrimmerTitleStatusUntrimming",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Untrimming {0} Title(s)...",
- "es_ES": "Deshaciendo Recorte de {0} Título(s)...",
- "fr_FR": "Dé-Réduction de {0} Fichier(s)...",
- "he_IL": "",
- "it_IT": "Ripristino alle dimensioni originali di {0} titolo/i...",
- "ja_JP": "",
- "ko_KR": "{0}개의 타이틀을 트리밍 해제 중...",
- "no_NO": "Untrimming {0} Tittel(er)...",
- "pl_PL": "",
- "pt_BR": "Desfazendo redução {0} Título(s)...",
- "ru_RU": "Отмена обрезки {0} файла(ов)...",
- "sv_SE": "Avoptimerar {0} spel...",
- "th_TH": "กำลังยกเลิกการตัดแต่ง {0} รายการ...",
- "tr_TR": "",
- "uk_UA": "Необрізаних {0} тайтл(ів)...",
- "zh_CN": "正在精简 {0} 个游戏",
- "zh_TW": "正在反修剪 {0} 個遊戲..."
- }
- },
- {
- "ID": "XCITrimmerTitleStatusFailed",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Failed",
- "es_ES": "Fallido",
- "fr_FR": "Échoué",
- "he_IL": "",
- "it_IT": "Fallito",
- "ja_JP": "",
- "ko_KR": "실패",
- "no_NO": "Mislyktes",
- "pl_PL": "",
- "pt_BR": "Falhou",
- "ru_RU": "Ошибка",
- "sv_SE": "Misslyckades",
- "th_TH": "ล้มเหลว",
- "tr_TR": "",
- "uk_UA": "Невдача",
- "zh_CN": "失败",
- "zh_TW": "失敗"
- }
- },
- {
- "ID": "XCITrimmerPotentialSavings",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Potential Savings",
- "es_ES": "Ahorro Potencial",
- "fr_FR": "Économies Potentielles",
- "he_IL": "",
- "it_IT": "Risparmio potenziale",
- "ja_JP": "",
- "ko_KR": "잠재적 비용 절감",
- "no_NO": "Potensielle besparelser",
- "pl_PL": "",
- "pt_BR": "Economia Potencial",
- "ru_RU": "Потенциальная экономия места",
- "sv_SE": "Möjlig besparning",
- "th_TH": "พื้นที่ประหยัดได้",
- "tr_TR": "",
- "uk_UA": "Потенційна економія",
- "zh_CN": "潜在的储存空间节省",
- "zh_TW": "潛在節省的儲存空間"
- }
- },
- {
- "ID": "XCITrimmerActualSavings",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Actual Savings",
- "es_ES": "Ahorro Real",
- "fr_FR": "Économies Réelles",
- "he_IL": "",
- "it_IT": "Risparmio effettivo",
- "ja_JP": "",
- "ko_KR": "실제 비용 절감",
- "no_NO": "Faktiske besparelser",
- "pl_PL": "",
- "pt_BR": "Economia Real",
- "ru_RU": "Фактическая экономия места",
- "sv_SE": "Faktisk besparning",
- "th_TH": "พื้นที่ที่ประหยัดจริง",
- "tr_TR": "",
- "uk_UA": "Зекономлено",
- "zh_CN": "实际的储存空间节省",
- "zh_TW": "實際節省的儲存空間"
- }
- },
- {
- "ID": "XCITrimmerSavingsMb",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "{0:n0} Mb",
- "es_ES": null,
- "fr_FR": "{0:n0} Mo",
- "he_IL": "",
- "it_IT": "{0:n0} MB",
- "ja_JP": "",
- "ko_KR": "{0:n0}MB",
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": null,
- "ru_RU": "{0:n0} Мб",
- "sv_SE": null,
- "th_TH": "{0:n0} เมกะไบต์",
- "tr_TR": "",
- "uk_UA": "{0:n0} Мб",
- "zh_CN": null,
- "zh_TW": null
- }
- },
- {
- "ID": "XCITrimmerSelectDisplayed",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Select Shown",
- "es_ES": "Seleccionar Mostrado(s)",
- "fr_FR": "Sélectionner Affiché",
- "he_IL": "",
- "it_IT": "Seleziona visualizzati",
- "ja_JP": "",
- "ko_KR": "표시됨 선택",
- "no_NO": "Velg vist",
- "pl_PL": "",
- "pt_BR": "Marcar Todos",
- "ru_RU": "Выбрать видимое",
- "sv_SE": "Markera visade",
- "th_TH": "เลือกรายการที่แสดง",
- "tr_TR": "",
- "uk_UA": "Вибрати показані",
- "zh_CN": "选定显示的",
- "zh_TW": "選擇已顯示"
- }
- },
- {
- "ID": "XCITrimmerDeselectDisplayed",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Deselect Shown",
- "es_ES": "Deseleccionar Mostrado(s)",
- "fr_FR": "Désélectionner Affiché",
- "he_IL": "",
- "it_IT": "Deseleziona visualizzati",
- "ja_JP": "",
- "ko_KR": "표시됨 선택 취소",
- "no_NO": "Opphev valg av Vist",
- "pl_PL": "",
- "pt_BR": "Desmarcar Todos",
- "ru_RU": "Отменить выбор видимого",
- "sv_SE": "Avmarkera visade",
- "th_TH": "ยกเลิกการเลือกรายการที่แสดง",
- "tr_TR": "",
- "uk_UA": "Скасувати вибір показаного",
- "zh_CN": "反选显示的",
- "zh_TW": "取消選擇已顯示"
- }
- },
- {
- "ID": "XCITrimmerSortName",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Title",
- "es_ES": "Título",
- "fr_FR": "Titre",
- "he_IL": "",
- "it_IT": "Titolo",
- "ja_JP": "",
- "ko_KR": "타이틀",
- "no_NO": "Tittel",
- "pl_PL": "",
- "pt_BR": "Título",
- "ru_RU": "Название",
- "sv_SE": "Titel",
- "th_TH": "ชื่อไฟล์",
- "tr_TR": "",
- "uk_UA": "Назва",
- "zh_CN": "标题",
- "zh_TW": "名稱"
- }
- },
- {
- "ID": "XCITrimmerSortSaved",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Space Savings",
- "es_ES": "Ahorro de Espacio",
- "fr_FR": "Économies de disque dur",
- "he_IL": "",
- "it_IT": "Spazio risparmiato",
- "ja_JP": "",
- "ko_KR": "공간 절약",
- "no_NO": "Plassbesparelser",
- "pl_PL": "",
- "pt_BR": "Economia de Espaço",
- "ru_RU": "Экономия места на диске",
- "sv_SE": "Utrymmesbesparning",
- "th_TH": "ลดการใช้พื้นที่",
- "tr_TR": "",
- "uk_UA": "Економія місця",
- "zh_CN": "节省空间",
- "zh_TW": "節省的儲存空間"
- }
- },
- {
- "ID": "XCITrimmerTrim",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Trim",
- "es_ES": "Recortar",
- "fr_FR": "Réduire",
- "he_IL": "",
- "it_IT": "Riduci dimensioni",
- "ja_JP": "",
- "ko_KR": "트림",
- "no_NO": "",
- "pl_PL": "",
- "pt_BR": "Reduzir XCI",
- "ru_RU": "Обрезать",
- "sv_SE": "Optimera",
- "th_TH": "ตัดแต่ง",
- "tr_TR": "",
- "uk_UA": "Обрізка",
- "zh_CN": "瘦身",
- "zh_TW": "修剪"
- }
- },
- {
- "ID": "XCITrimmerUntrim",
- "Translations": {
- "ar_SA": "",
- "de_DE": "",
- "el_GR": "",
- "en_US": "Untrim",
- "es_ES": "Deshacer Recorte",
- "fr_FR": "Dé-Réduire",
- "he_IL": "",
- "it_IT": "Riporta alle dimensioni originali",
- "ja_JP": "",
- "ko_KR": "언트림",
- "no_NO": "Utrim",
- "pl_PL": "",
- "pt_BR": "Desfazer Redução",
- "ru_RU": "Отменить обрезку",
- "sv_SE": "Avoptimera",
- "th_TH": "ยกเลิกการตัดแต่ง",
- "tr_TR": "",
- "uk_UA": "Зшивання",
- "zh_CN": "取消精简",
- "zh_TW": "反修剪"
+ "uk_UA": "{0} нових оновлень додано",
+ "zh_CN": "{0} 个更新被添加",
+ "zh_TW": "已加入 {0} 個遊戲更新"
}
},
{
"ID": "UpdateWindowBundledContentNotice",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Gebündelte Updates können nicht entfernt, sondern nur deaktiviert werden.",
"el_GR": "",
"en_US": "Bundled updates cannot be removed, only disabled.",
"es_ES": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.",
@@ -17229,7 +15804,7 @@
"ID": "BuildId",
"Translations": {
"ar_SA": "معرف البناء:",
- "de_DE": "",
+ "de_DE": "Build-ID:",
"el_GR": "",
"en_US": "BuildId:",
"es_ES": "Id de compilación:",
@@ -17254,7 +15829,7 @@
"ID": "DlcWindowBundledContentNotice",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Gebündelte DLC können nicht entfernt, sondern nur deaktiviert werden.",
"el_GR": "",
"en_US": "Bundled DLC cannot be removed, only disabled.",
"es_ES": "El DLC incluido no se puede eliminar, solo desactivar.",
@@ -17279,7 +15854,7 @@
"ID": "DlcWindowHeading",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "{0} DLC(s) verfügbar",
"el_GR": "",
"en_US": "{0} DLC(s) Available",
"es_ES": "{0} DLC(s) disponible(s)",
@@ -17354,7 +15929,7 @@
"ID": "Continue",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Fortfahren",
"el_GR": "",
"en_US": "Continue",
"es_ES": "Continuar",
@@ -17554,7 +16129,7 @@
"ID": "GameUpdateWindowHeading",
"Translations": {
"ar_SA": "إدارة التحديثات لـ {0} ({1})",
- "de_DE": "Update verfügbar für {0} [{1}]",
+ "de_DE": "Verwalte Updates für {0} [{1}]",
"el_GR": "{0} Update(s) available for {1} ({2})",
"en_US": "Manage Updates for {0} ({1})",
"es_ES": "Actualizaciones Disponibles para {0} [{1}]",
@@ -17854,7 +16429,7 @@
"ID": "SettingsShowConsoleRestartMessage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Die Konsole wird beim nächsten Start von Ryujinx verfügbar sein.",
"el_GR": "",
"en_US": "The console will be available the next time Ryujinx starts.",
"es_ES": "La consola estará disponible la próxima vez que se inicie Ryujinx.",
@@ -18104,7 +16679,7 @@
"ID": "VolumeShort",
"Translations": {
"ar_SA": "مستوى",
- "de_DE": "",
+ "de_DE": "Lautstärke",
"el_GR": "Έντ.",
"en_US": "Vol",
"es_ES": "Volumen",
@@ -18250,31 +16825,6 @@
"zh_TW": "存檔管理員"
}
},
- {
- "ID": "Name",
- "Translations": {
- "ar_SA": "الاسم",
- "de_DE": "",
- "el_GR": "Όνομα",
- "en_US": "Name",
- "es_ES": "Nombre",
- "fr_FR": "Nom",
- "he_IL": "שם",
- "it_IT": "Nome",
- "ja_JP": "名称",
- "ko_KR": "이름",
- "no_NO": "Navn",
- "pl_PL": "Nazwa",
- "pt_BR": "Nome",
- "ru_RU": "Название",
- "sv_SE": "Namn",
- "th_TH": "ชื่อ",
- "tr_TR": "İsim",
- "uk_UA": "Назва",
- "zh_CN": "名称",
- "zh_TW": "名稱"
- }
- },
{
"ID": "Size",
"Translations": {
@@ -18300,31 +16850,6 @@
"zh_TW": "大小"
}
},
- {
- "ID": "Search",
- "Translations": {
- "ar_SA": "بحث",
- "de_DE": "Suche",
- "el_GR": "Αναζήτηση",
- "en_US": "Search",
- "es_ES": "Buscar",
- "fr_FR": "Rechercher",
- "he_IL": "חפש",
- "it_IT": "Cerca",
- "ja_JP": "検索",
- "ko_KR": "찾기",
- "no_NO": "Søk",
- "pl_PL": "Wyszukaj",
- "pt_BR": "Buscar",
- "ru_RU": "Поиск",
- "sv_SE": "Sök",
- "th_TH": "ค้นหา",
- "tr_TR": "Ara",
- "uk_UA": "Пошук",
- "zh_CN": "搜索",
- "zh_TW": "搜尋"
- }
- },
{
"ID": "UserProfilesRecoverLostAccounts",
"Translations": {
@@ -18854,7 +17379,7 @@
"ID": "NetworkInterfaceTooltip",
"Translations": {
"ar_SA": "واجهة الشبكة مستخدمة لميزات LAN/LDN.\n\nبالاشتراك مع VPN أو XLink Kai ولعبة تدعم LAN، يمكن استخدامها لتزييف اتصال الشبكة نفسها عبر الإنترنت.\n\nاتركه على الافتراضي إذا لم تكن متأكدا.",
- "de_DE": "Die für LAN/LDN-Funktionen verwendete Netzwerkschnittstelle.\n\nIn Verbindung mit einem VPN oder XLink Kai und einem Spiel mit LAN-Unterstützung kann eine Verbindung mit demselben Netzwerk über das Internet vorgetäuscht werden.\n\nIm Zweifelsfall auf DEFAULT belassen.",
+ "de_DE": "Die für LAN/LDN-Funktionen verwendete Netzwerkschnittstelle.\n\nIn Verbindung mit einem VPN oder XLink Kai und einem Spiel mit LAN-Unterstützung kann eine Verbindung mit demselben Netzwerk über das Internet vorgetäuscht werden.\n\nIm Zweifelsfall auf STANDARD belassen.",
"el_GR": "",
"en_US": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.",
"es_ES": "Interfaz de red usada para características LAN/LDN.\n\njunto con una VPN o XLink Kai y un juego con soporte LAN, puede usarse para suplantar una conexión de la misma red a través de Internet.\n\nDeje en DEFAULT si no está seguro.",
@@ -18929,7 +17454,7 @@
"ID": "AboutChangelogButton",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Changelog anzeigen",
"el_GR": "",
"en_US": "View Changelog",
"es_ES": "Ver el Changelog",
@@ -19029,7 +17554,7 @@
"ID": "MultiplayerModeTooltip",
"Translations": {
"ar_SA": "تغيير وضع LDN متعدد اللاعبين.\n\nسوف يقوم ldn_mitm بتعديل وظيفة اللعب المحلية/اللاسلكية المحلية في الألعاب لتعمل كما لو كانت شبكة LAN، مما يسمح باتصالات الشبكة المحلية نفسها مع محاكيات ريوجينكس الأخرى وأجهزة نينتندو سويتش المخترقة التي تم تثبيت وحدة ldn_mitm عليها.\n\nيتطلب وضع اللاعبين المتعددين أن يكون جميع اللاعبين على نفس إصدار اللعبة (على سبيل المثال، يتعذر على الإصدار 13.0.1 من سوبر سماش برذرز ألتميت الاتصال بالإصدار 13.0.0).\n\nاتركه معطلا إذا لم تكن متأكدا.",
- "de_DE": "Ändert den LDN-Mehrspielermodus.\n\nldn_mitm ändert die lokale drahtlose/lokale Spielfunktionalität in Spielen so, dass sie wie ein LAN funktioniert und lokale, netzwerkgleiche Verbindungen mit anderen Ryujinx-Instanzen und gehackten Nintendo Switch-Konsolen ermöglicht, auf denen das ldn_mitm-Modul installiert ist.\n\nMultiplayer erfordert, dass alle Spieler die gleiche Spielversion verwenden (d.h. Super Smash Bros. Ultimate v13.0.1 kann sich nicht mit v13.0.0 verbinden).\n\nIm Zweifelsfall auf DISABLED lassen.",
+ "de_DE": "Ändert den LDN-Mehrspielermodus.\n\nldn_mitm ändert die lokale drahtlose/lokale Spielfunktionalität in Spielen so, dass sie wie ein LAN funktioniert und lokale, netzwerkgleiche Verbindungen mit anderen Ryujinx-Instanzen und gehackten Nintendo Switch-Konsolen ermöglicht, auf denen das ldn_mitm-Modul installiert ist.\n\nMultiplayer erfordert, dass alle Spieler die gleiche Spielversion verwenden (d.h. Super Smash Bros. Ultimate v13.0.1 kann sich nicht mit v13.0.0 verbinden).\n\nIm Zweifelsfall auf DEAKTIVIERT lassen.",
"el_GR": "",
"en_US": "Change LDN multiplayer mode.\n\nldn_mitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
"es_ES": "Cambiar modo LDN multijugador.\n\nldn_mitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.",
@@ -19504,7 +18029,7 @@
"ID": "SettingsTabSystemVSyncModeCustom",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Benutzerdefinierte Bildwiederholrate",
"el_GR": "",
"en_US": "Custom Refresh Rate",
"es_ES": "Frecuencia de Actualización Personalizada",
@@ -19704,7 +18229,7 @@
"ID": "SettingsTabSystemCustomVSyncInterval",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Intervall",
"el_GR": "",
"en_US": "Interval",
"es_ES": "Intervalo",
@@ -19804,7 +18329,7 @@
"ID": "SettingsTabHotkeysTurboMode",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Turbo-Modus",
"el_GR": "",
"en_US": "Turbo Mode:",
"es_ES": "Modo Turbo:",
@@ -19829,7 +18354,7 @@
"ID": "SettingsTabHotkeysTurboModeToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Der Turbo-Modus Hotkey.\nKonfigurieren Sie das Verhalten des Turbo-Modus in den CPU-Einstellungen von Ryujinx.\n\nIm Zweifelsfall Ungebunden lassen.",
"el_GR": "",
"en_US": "The Turbo mode hotkey.\nConfigure the behavior of Turbo mode in Ryujinx CPU settings.\n\nLeave Unbound if unsure.",
"es_ES": "La tecla de acceso rápido del modo Turbo.\nConfigura el comportamiento del modo Turbo en los ajustes de CPU de Ryujinx.\n\nDejar sin asignar si no está seguro.",
@@ -19854,7 +18379,7 @@
"ID": "SettingsTabHotkeysOnlyWhilePressed",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Nur bei gedrückter Taste",
"el_GR": "",
"en_US": "Only While Pressed",
"es_ES": "Solo Mientras se Mantiene Pulsado",
@@ -19904,7 +18429,7 @@
"ID": "CompatibilityListTitle",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Kompatibilitätsliste - {0} Einträge",
"el_GR": "",
"en_US": "Compatibility List - {0} entries",
"es_ES": "Lista de Compatibilidad - {0} entradas",
@@ -19929,7 +18454,7 @@
"ID": "CompatibilityListWarning",
"Translations": {
"ar_SA": "",
- "de_DE": "Diese Kompatibilitätsliste könnte veraltete Einträge enthalten. Teste dennoch Spiele im \"Ingame\"-Status.",
+ "de_DE": "Diese Kompatibilitätsliste könnte veraltete Einträge enthalten.\nTeste dennoch Spiele im \"Ingame\"-Status.",
"el_GR": "",
"en_US": "This compatibility list might contain out of date entries.\nDo not be opposed to testing games in the \"Ingame\" status.",
"es_ES": "Esta lista de compatibilidad podría contener entradas desactualizadas.\nNo dudes en probar los juegos en estado \"En el juego\".",
@@ -19979,7 +18504,7 @@
"ID": "CompatibilityListSearchBoxWatermarkWithCount",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Durchsuche {0} Kompatibilitätseinträge...",
"el_GR": "",
"en_US": "Search {0} compatibility entries...",
"es_ES": "Buscar {0} entradas de compatibilidad…",
@@ -20004,7 +18529,7 @@
"ID": "CompatibilityListOpen",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Kompatibilitätsliste",
"el_GR": "",
"en_US": "Compatibility List",
"es_ES": "Lista de Compatibilidad",
@@ -20204,7 +18729,7 @@
"ID": "CompatibilityListMenus",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Menüs",
"el_GR": "",
"en_US": "Menus",
"es_ES": "Menu",
@@ -20229,7 +18754,7 @@
"ID": "CompatibilityListBoots",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Startet",
"el_GR": "",
"en_US": "Boots",
"es_ES": "Inicia",
@@ -20254,7 +18779,7 @@
"ID": "CompatibilityListNothing",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Nichts",
"el_GR": "",
"en_US": "Nothing",
"es_ES": "Nada",
@@ -20279,7 +18804,7 @@
"ID": "CompatibilityListPlayableTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Startet und läuft ohne Abstürze oder GPU-Bugs jeglicher Art und mit einer Geschwindigkeit, die auf einem durchschnittlichen PC ein angenehmes Spielerlebnis ermöglicht.",
"el_GR": "",
"en_US": "Boots and plays without any crashes or GPU bugs of any kind, and at a speed fast enough to reasonably enjoy on an average PC.",
"es_ES": "Se inicia y funciona sin ningún fallo ni error de GPU, y a una velocidad lo suficientemente rápida como para disfrutarlo razonablemente en un PC promedio",
@@ -20304,7 +18829,7 @@
"ID": "CompatibilityListIngameTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Startet und läuft im Spiel, leidet jedoch unter einem oder mehreren der folgenden Probleme: Abstürze, Deadlocks, GPU-Bugs, störend schlechter Sound oder ist einfach zu langsam. Das Spiel kann möglicherweise trotzdem komplett durchgespielt werden, aber nicht so, wie es vorgesehen ist.",
"el_GR": "",
"en_US": "Boots and goes in-game but suffers from one or more of the following: crashes, deadlocks, GPU bugs, distractingly bad audio, or is simply too slow. Game still might able to be played all the way through, but not as the game is intended to play.",
"es_ES": "Se inicia y llega al juego, pero presenta uno o más de los siguientes problemas: cierres inesperados, bloqueos, errores de GPU, audio molesto o deficiente, o simplemente es demasiado lento. El juego aún podría jugarse hasta el final, pero no como fue pensado originalmente.",
@@ -20329,7 +18854,7 @@
"ID": "CompatibilityListMenusTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Startet und gelangt über den Titelbildschirm hinaus, erreicht jedoch nicht das eigentliche Gameplay.",
"el_GR": "",
"en_US": "Boots and goes past the title screen but does not make it into main gameplay.",
"es_ES": "Se inicia y pasa de la pantalla de título, pero no llega al juego principal.",
@@ -20479,7 +19004,7 @@
"ID": "GameInfoRpcImage",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Rich-Presence-Bild",
"el_GR": "",
"en_US": "Rich Presence Image",
"es_ES": "Imagen Rich Presence",
@@ -20504,7 +19029,7 @@
"ID": "GameInfoRpcDynamic",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Dynamische Rich Presence",
"el_GR": "",
"en_US": "Dynamic Rich Presence",
"es_ES": "Rich Presence Dinámico",
@@ -20579,7 +19104,7 @@
"ID": "SettingsTabDebugNote",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "WARNUNG: Nur für Entwickler vorgesehen, wird die Leistung verringern",
"el_GR": "",
"en_US": "WARNING: For developer use only, will reduce performance",
"es_ES": "ADVERTENCIA: Solo para uso de desarrolladores, reducirá el rendimiento",
@@ -20604,7 +19129,7 @@
"ID": "SettingsTabDebugEnableGDBStub",
"Translations": {
"ar_SA": "",
- "de_DE": "GDB Stub aktivieren",
+ "de_DE": "GDB-Stub aktivieren",
"el_GR": "",
"en_US": "Enable GDB Stub",
"es_ES": "Activar Stub GDB",
@@ -20629,7 +19154,7 @@
"ID": "SettingsTabDebugGDBStubToggleTooltip",
"Translations": {
"ar_SA": "",
- "de_DE": "Aktiviert den GDB Stub, welcher debugging an der Anwendung ermöglicht. Nur für Entwicklerzwecke!",
+ "de_DE": "Aktiviert den GDB-Stub, welcher debugging an der Anwendung ermöglicht. Nur für Entwicklerzwecke!",
"el_GR": "",
"en_US": "Enables the GDB stub which makes it possible to debug the running application. For development use only!",
"es_ES": "Activa el stub GDB, lo que permite depurar la aplicación en ejecución. ¡Solo para uso de desarrollo!",
@@ -20654,7 +19179,7 @@
"ID": "SettingsTabDebugGDBStubPort",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "GDB-Stub-Port:",
"el_GR": "",
"en_US": "GDB Stub Port:",
"es_ES": "Puerto del Stub GDB:",
diff --git a/assets/Locales/Settings_Interface.json b/assets/Locales/Settings_Interface.json
index adab7755d..e9896cd69 100644
--- a/assets/Locales/Settings_Interface.json
+++ b/assets/Locales/Settings_Interface.json
@@ -54,7 +54,7 @@
"ID": "StartGamesWithoutUICheckboxLabelToolTip",
"Translations": {
"ar_SA": "",
- "de_DE": "",
+ "de_DE": "Blendet die Benutzeroberfläche von Ryujinx automatisch aus, sobald ein Spiel gestartet wird. Während des Spiels kann die Oberfläche mit F4 eingeblendet werden.",
"el_GR": "",
"en_US": "Automatically hides Ryujinx's UI whenever a game launches. While in-game, press F4 to show the UI.",
"es_ES": "",
diff --git a/assets/Locales/StatusBar.json b/assets/Locales/StatusBar.json
index 174efe010..2ffe565e3 100644
--- a/assets/Locales/StatusBar.json
+++ b/assets/Locales/StatusBar.json
@@ -1,7 +1,32 @@
{
"Locales": [
{
- "ID": "FirmwareVersion",
+ "ID": "TrimmingXCILabel",
+ "Translations": {
+ "ar_SA": "جاري تقليم: {0}",
+ "de_DE": "Schneide: {0}",
+ "el_GR": "Κόβει το: {0}",
+ "en_US": "Trimming: {0}",
+ "es_ES": "Recortando: {0}",
+ "fr_FR": "Réduction de: {0}",
+ "he_IL": "חיתוך: {0}",
+ "it_IT": "Riduzione di: {0}",
+ "ja_JP": "{0} をトリミング中:",
+ "ko_KR": "{0} 트리밍:",
+ "no_NO": "Trimming av: {0}",
+ "pl_PL": "Przycinanie: {0}",
+ "pt_BR": "Reduzindo: {0}",
+ "ru_RU": "Обрезка: {0}",
+ "sv_SE": "Trimmar: {0}",
+ "th_TH": "กำลังตัด: {0}",
+ "tr_TR": "{0} Kısaltılıyor:",
+ "uk_UA": "Обрізка: {0}",
+ "zh_CN": "正在修剪: {0}",
+ "zh_TW": "正在修剪: {0}"
+ }
+ },
+ {
+ "ID": "FirmwareVersionLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
diff --git a/assets/Locales/XCITrimmer.json b/assets/Locales/XCITrimmer.json
new file mode 100644
index 000000000..53ba81242
--- /dev/null
+++ b/assets/Locales/XCITrimmer.json
@@ -0,0 +1,429 @@
+{
+ "Locales": [
+ {
+ "ID": "StatusCountLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI ausgewählt: {0}/{1}",
+ "el_GR": "",
+ "en_US": "XCI Selected: {0}/{1}",
+ "es_ES": "XCI Seleccionados: {0}/{1}",
+ "fr_FR": "XCI Sélectionnés : {0}/{1}",
+ "he_IL": "",
+ "it_IT": "XCI Selezionati: {0}/{1}",
+ "ja_JP": "",
+ "ko_KR": "XCI 선택됨: {0}/{1}",
+ "no_NO": "XCI Valgt: {0}/{1}",
+ "pl_PL": "XCI Wybrane: {0}/{1}",
+ "pt_BR": "XCI Selecionados: {0}/{1}",
+ "ru_RU": "Выбрано XCI: {0}/{1}",
+ "sv_SE": "XCI Valda: {0}/{1}",
+ "th_TH": "XCI เลือกแล้ว: {0}/{1}",
+ "tr_TR": "",
+ "uk_UA": "Вибрано XCI: {0}/{1}",
+ "zh_CN": "XCI 已选: {0}/{1}",
+ "zh_TW": "XCI 已選擇: {0}/{1}"
+ }
+ },
+ {
+ "ID": "StatusCountWithFilterLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "XCI ausgewählt: {0}/{1} (Sichtbar: {2})",
+ "el_GR": "",
+ "en_US": "XCI Selected: {0}/{1} (Displayed: {2})",
+ "es_ES": "XCI Seleccionados: {0}/{1} (Mostrados: {2})",
+ "fr_FR": "XCI Sélectionnés : {0}/{1} (Affichés : {2})",
+ "he_IL": "",
+ "it_IT": "XCI Selezionati: {0}/{1} (Visualizzati: {2})",
+ "ja_JP": "",
+ "ko_KR": "XCI 선택됨: {0}/{1} (표시됨: {2})",
+ "no_NO": "XCI Valgt: {0}/{1} (Vises: {2})",
+ "pl_PL": "XCI Wybrane: {0}/{1} (Wyświetlone: {2})",
+ "pt_BR": "XCI Selecionados: {0}/{1} (Exibidos: {2})",
+ "ru_RU": "Выбрано XCI: {0}/{1} (Отображается: {2})",
+ "sv_SE": "XCI Valda: {0}/{1} (Visas: {2})",
+ "th_TH": "XCI เลือกแล้ว: {0}/{1} (แสดง: {2})",
+ "tr_TR": "",
+ "uk_UA": "Вибрано XCI: {0}/{1} (Відображається: {2})",
+ "zh_CN": "XCI 已选: {0}/{1}(显示: {2})",
+ "zh_TW": "XCI 已選擇: {0}/{1}(顯示: {2})"
+ }
+ },
+ {
+ "ID": "StatusTrimmingLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmen: {0}/{1}...",
+ "el_GR": "",
+ "en_US": "Trimming: {0}/{1}...",
+ "es_ES": "Recortando: {0}/{1}...",
+ "fr_FR": "Réduction : {0}/{1}...",
+ "he_IL": "",
+ "it_IT": "Riduzione: {0}/{1}...",
+ "ja_JP": "",
+ "ko_KR": "트리밍: {0}/{1}...",
+ "no_NO": "Trimmer: {0}/{1}...",
+ "pl_PL": "",
+ "pt_BR": "Recortando: {0}/{1}...",
+ "ru_RU": "Обрезка: {0}/{1}...",
+ "sv_SE": "Trimmar: {0}/{1}...",
+ "th_TH": "กำลังตัดแต่ง: {0}/{1}...",
+ "tr_TR": "",
+ "uk_UA": "Обрізання: {0}/{1}...",
+ "zh_CN": "正在修剪:{0}/{1}...",
+ "zh_TW": "正在修剪:{0}/{1}..."
+ }
+ },
+ {
+ "ID": "StatusUntrimmingLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmung von {0} wieder aufheben...",
+ "el_GR": "",
+ "en_US": "Untrimming {0}...",
+ "es_ES": "Deshaciendo el recorte {0}...",
+ "fr_FR": "Restauration de {0}...",
+ "he_IL": "",
+ "it_IT": "Ripristino di {0}...",
+ "ja_JP": "",
+ "ko_KR": "{0} 복원 중...",
+ "no_NO": "Gjenoppretter {0}...",
+ "pl_PL": "",
+ "pt_BR": "Restaurando {0}...",
+ "ru_RU": "Восстановление {0}...",
+ "sv_SE": "Återställer {0}...",
+ "th_TH": "กำลังกู้คืน {0}...",
+ "tr_TR": "",
+ "uk_UA": "Відновлення {0}...",
+ "zh_CN": "正在恢复 {0}...",
+ "zh_TW": "正在還原 {0}..."
+ }
+ },
+ {
+ "ID": "SelectAllButton",
+ "Translations": {
+ "ar_SA": "اختر الكل",
+ "de_DE": "Alles auswählen",
+ "el_GR": "Επιλογή όλων",
+ "en_US": "Select All",
+ "es_ES": "Seleccionar Todo",
+ "fr_FR": "Sélectionner Tout",
+ "he_IL": "בחר הכל",
+ "it_IT": "Seleziona tutto",
+ "ja_JP": "すべて選択",
+ "ko_KR": "모두 선택",
+ "no_NO": "Velg alle",
+ "pl_PL": "Zaznacz wszystko",
+ "pt_BR": "Selecionar tudo",
+ "ru_RU": "Выбрать все",
+ "sv_SE": "Markera alla",
+ "th_TH": "เลือกทั้งหมด",
+ "tr_TR": "Hepsini seç",
+ "uk_UA": "Вибрати все",
+ "zh_CN": "选择全部",
+ "zh_TW": "選擇全部"
+ }
+ },
+ {
+ "ID": "ClearSelectionButton",
+ "Translations": {
+ "ar_SA": "مسح التحديد",
+ "de_DE": "Auswahl aufheben",
+ "el_GR": "Εκκαθάριση επιλογής",
+ "en_US": "Clear Selection",
+ "es_ES": "Borrar selección",
+ "fr_FR": "Effacer la sélection",
+ "he_IL": "נקה בחירה",
+ "it_IT": "Cancella selezione",
+ "ja_JP": "選択をクリア",
+ "ko_KR": "선택 해제",
+ "no_NO": "Fjern utvalg",
+ "pl_PL": "Wyczyść zaznaczenie",
+ "pt_BR": "Limpar seleção",
+ "ru_RU": "Очистить выделение",
+ "sv_SE": "Rensa markering",
+ "th_TH": "ล้างการเลือก",
+ "tr_TR": "Seçimi temizle",
+ "uk_UA": "Очистити вибір",
+ "zh_CN": "清除选择",
+ "zh_TW": "清除選擇"
+ }
+ },
+ {
+ "ID": "TrimmedLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Getrimmt",
+ "el_GR": "",
+ "en_US": "Trimmed",
+ "es_ES": "Recortado",
+ "fr_FR": "Réduit",
+ "he_IL": "",
+ "it_IT": "Dim. ridotta",
+ "ja_JP": "",
+ "ko_KR": "트리밍됨",
+ "no_NO": "Trimmet",
+ "pl_PL": "",
+ "pt_BR": "Reduzido",
+ "ru_RU": "Обрезан",
+ "sv_SE": "Optimerad",
+ "th_TH": "ตัดแต่งแล้ว",
+ "tr_TR": "",
+ "uk_UA": "Обрізані",
+ "zh_CN": "经过瘦身的",
+ "zh_TW": "已修剪"
+ }
+ },
+ {
+ "ID": "UntrimmedLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Ungetrimmt",
+ "el_GR": "",
+ "en_US": "Untrimmed",
+ "es_ES": "Sin Recortar",
+ "fr_FR": "Non Réduit",
+ "he_IL": "",
+ "it_IT": "Dim. originale",
+ "ja_JP": "",
+ "ko_KR": "트리밍되지 않음",
+ "no_NO": "Ikke trimmet",
+ "pl_PL": "",
+ "pt_BR": "Não Reduzido",
+ "ru_RU": "Не обрезан",
+ "sv_SE": "Orörd",
+ "th_TH": "ยังไม่ได้ตัดแต่ง",
+ "tr_TR": "",
+ "uk_UA": "Необрізані",
+ "zh_CN": "没有瘦身的",
+ "zh_TW": "未修剪"
+ }
+ },
+ {
+ "ID": "PartialLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Teilweise",
+ "el_GR": "",
+ "en_US": "Partial",
+ "es_ES": "Parcial",
+ "fr_FR": "Partiel",
+ "he_IL": "",
+ "it_IT": "Parziale",
+ "ja_JP": "",
+ "ko_KR": "일부",
+ "no_NO": "Delvis",
+ "pl_PL": "",
+ "pt_BR": "Parcial",
+ "ru_RU": "Частично",
+ "sv_SE": "Delvis",
+ "th_TH": "ยังไม่สมบูรณ์",
+ "tr_TR": "",
+ "uk_UA": "Часткові",
+ "zh_CN": "分区",
+ "zh_TW": "部分"
+ }
+ },
+ {
+ "ID": "FailedLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Fehlgeschlagen",
+ "el_GR": "",
+ "en_US": "Failed",
+ "es_ES": "Fallido",
+ "fr_FR": "Échoué",
+ "he_IL": "",
+ "it_IT": "Fallito",
+ "ja_JP": "",
+ "ko_KR": "실패",
+ "no_NO": "Mislyktes",
+ "pl_PL": "",
+ "pt_BR": "Falhou",
+ "ru_RU": "Ошибка",
+ "sv_SE": "Misslyckades",
+ "th_TH": "ล้มเหลว",
+ "tr_TR": "",
+ "uk_UA": "Невдача",
+ "zh_CN": "失败",
+ "zh_TW": "失敗"
+ }
+ },
+ {
+ "ID": "UnknownLabel",
+ "Translations": {
+ "ar_SA": "مجهول",
+ "de_DE": "Unbekannt",
+ "el_GR": "Άγνωστο",
+ "en_US": "Unknown",
+ "es_ES": "Desconocido",
+ "fr_FR": "Inconnu",
+ "he_IL": "לא ידוע",
+ "it_IT": "Sconosciuto",
+ "ja_JP": "不明",
+ "ko_KR": "알 수 없음",
+ "no_NO": "Ukjent",
+ "pl_PL": "Nieznany",
+ "pt_BR": "Desconhecido",
+ "ru_RU": "Неизвестно",
+ "sv_SE": "Okänd",
+ "th_TH": "ไม่รู้จัก",
+ "tr_TR": "Bilinmeyen",
+ "uk_UA": "Невідомо",
+ "zh_CN": "未知",
+ "zh_TW": "未知"
+ }
+ },
+ {
+ "ID": "CalculatedSavingsLabel",
+ "Translations": {
+ "ar_SA": null,
+ "de_DE": null,
+ "el_GR": null,
+ "en_US": "{0} MB ({1}%)",
+ "es_ES": null,
+ "fr_FR": "{0} Mo ({1} %)",
+ "he_IL": null,
+ "it_IT": null,
+ "ja_JP": "{0}MB({1}%)",
+ "ko_KR": "{0}MB ({1}%)",
+ "no_NO": null,
+ "pl_PL": null,
+ "pt_BR": null,
+ "ru_RU": "{0} МБ ({1}%)",
+ "sv_SE": null,
+ "th_TH": null,
+ "tr_TR": null,
+ "uk_UA": "{0} МБ ({1}%)",
+ "zh_CN": "{0} MB({1}%)",
+ "zh_TW": "{0} MB({1}%)"
+ }
+ },
+ {
+ "ID": "SavedLabel",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Gespeichert:",
+ "el_GR": "",
+ "en_US": "Saved:",
+ "es_ES": "Ahorrado:",
+ "fr_FR": "Économies :",
+ "he_IL": "",
+ "it_IT": "Risparmiato:",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "Zaoszczędzone:",
+ "pt_BR": "Economizado:",
+ "ru_RU": "Сохранено:",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "Збережено:",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "RemainingLabel",
+ "Translations": {
+ "ar_SA": "المتبقية:",
+ "de_DE": "Verbleibend:",
+ "el_GR": "Απομένουν:",
+ "en_US": "Remaining:",
+ "es_ES": "Restante:",
+ "fr_FR": "Restant :",
+ "he_IL": "נותרו:",
+ "it_IT": "Rimanenti:",
+ "ja_JP": "残り:",
+ "ko_KR": "남은:",
+ "no_NO": "Gjenstående:",
+ "pl_PL": "Pozostało:",
+ "pt_BR": "Restante:",
+ "ru_RU": "Осталось:",
+ "sv_SE": "Kvar:",
+ "th_TH": "เหลือ:",
+ "tr_TR": "Kalan:",
+ "uk_UA": "Залишилося:",
+ "zh_CN": "剩余:",
+ "zh_TW": "剩餘:"
+ }
+ },
+ {
+ "ID": "MBLabel",
+ "Translations": {
+ "ar_SA": null,
+ "de_DE": null,
+ "el_GR": null,
+ "en_US": "{0} MB",
+ "es_ES": null,
+ "fr_FR": "{0} Mo",
+ "he_IL": null,
+ "it_IT": null,
+ "ja_JP": "{0}MB",
+ "ko_KR": "{0}MB",
+ "no_NO": null,
+ "pl_PL": null,
+ "pt_BR": null,
+ "ru_RU": "{0} МБ",
+ "sv_SE": null,
+ "th_TH": null,
+ "tr_TR": null,
+ "uk_UA": "{0} МБ",
+ "zh_CN": null,
+ "zh_TW": null
+ }
+ },
+ {
+ "ID": "TrimButton",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmen",
+ "el_GR": "",
+ "en_US": "Trim",
+ "es_ES": "Recortar",
+ "fr_FR": "Réduire",
+ "he_IL": "",
+ "it_IT": "Riduci",
+ "ja_JP": "",
+ "ko_KR": "트림",
+ "no_NO": "",
+ "pl_PL": "Przytnij",
+ "pt_BR": "Reduzir",
+ "ru_RU": "Обрезать",
+ "sv_SE": "Trimma",
+ "th_TH": "ตัด",
+ "tr_TR": "Kırp",
+ "uk_UA": "Обрізати",
+ "zh_CN": "瘦身",
+ "zh_TW": "修剪"
+ }
+ },
+ {
+ "ID": "UntrimButton",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "Trimmen aufheben",
+ "el_GR": "",
+ "en_US": "Untrim",
+ "es_ES": "Desrecortar",
+ "fr_FR": "Dé-Réduire",
+ "he_IL": "",
+ "it_IT": "Ripristina",
+ "ja_JP": "",
+ "ko_KR": "언트림",
+ "no_NO": "Utrim",
+ "pl_PL": "",
+ "pt_BR": "Restaurar",
+ "ru_RU": "Восстановить",
+ "sv_SE": "Avoptimera",
+ "th_TH": "กู้คืน",
+ "tr_TR": "",
+ "uk_UA": "Відновити",
+ "zh_CN": "取消精简",
+ "zh_TW": "反修剪"
+ }
+ }
+ ]
+}
diff --git a/docs/compatibility.csv b/docs/compatibility.csv
index a0a7e730a..b24ffd31b 100644
--- a/docs/compatibility.csv
+++ b/docs/compatibility.csv
@@ -2667,7 +2667,8 @@
0100C52011460000,"Sky: Children of the Light",cpu;online-broken,nothing,2023-02-23 10:57:10
010041C01014E000,"Skybolt Zack",,playable,2021-04-12 18:28:00
0100A0A00D1AA000,"SKYHILL",,playable,2021-03-05 15:19:11
-0100CCC0002E6000,"Skylanders Imaginators",crash;services,boots,2020-05-30 18:49:18
+010048D000BFC000,"Skylanders Imaginators",,playable,2026-07-02 22:43:30
+0100CCC0002E6000,"Skylanders Imaginators",,playable,2026-07-02 22:43:30
010021A00ABEE000,"SKYPEACE",,playable,2020-05-29 14:14:30
0100EA400BF44000,"SkyScrappers",,playable,2020-05-28 22:11:25
0100F3C00C400000,"SkyTime",slow,ingame,2020-05-30 09:24:51
diff --git a/src/ARMeilleure/CodeGen/CompiledFunction.cs b/src/ARMeilleure/CodeGen/CompiledFunction.cs
index 7014f715a..a5629b718 100644
--- a/src/ARMeilleure/CodeGen/CompiledFunction.cs
+++ b/src/ARMeilleure/CodeGen/CompiledFunction.cs
@@ -8,7 +8,7 @@ namespace ARMeilleure.CodeGen
///
/// Represents a compiled function.
///
- readonly struct CompiledFunction
+ public readonly struct CompiledFunction
{
///
/// Gets the machine code of the .
@@ -44,10 +44,11 @@ namespace ARMeilleure.CodeGen
/// pointing to the mapped function.
///
/// Type of delegate
+ /// The jit cache to map the function into
/// A delegate of type pointing to the mapped function
- public T Map()
+ public T Map(JitCache jitCache)
{
- return MapWithPointer(out _);
+ return MapWithPointer(jitCache, out _);
}
///
@@ -55,11 +56,12 @@ namespace ARMeilleure.CodeGen
/// pointing to the mapped function.
///
/// Type of delegate
+ /// The jit cache to map the function into
/// Pointer to the function code in memory
/// A delegate of type pointing to the mapped function
- public T MapWithPointer(out nint codePointer)
+ public T MapWithPointer(JitCache jitCache, out nint codePointer)
{
- codePointer = JitCache.Map(this);
+ codePointer = jitCache.Map(this);
return Marshal.GetDelegateForFunctionPointer(codePointer);
}
diff --git a/src/ARMeilleure/CodeGen/Linking/RelocEntry.cs b/src/ARMeilleure/CodeGen/Linking/RelocEntry.cs
index d103bc395..b78b9edd3 100644
--- a/src/ARMeilleure/CodeGen/Linking/RelocEntry.cs
+++ b/src/ARMeilleure/CodeGen/Linking/RelocEntry.cs
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
///
/// Represents a relocation.
///
- readonly struct RelocEntry
+ public readonly struct RelocEntry
{
public const int Stride = 13; // Bytes.
diff --git a/src/ARMeilleure/CodeGen/Linking/RelocInfo.cs b/src/ARMeilleure/CodeGen/Linking/RelocInfo.cs
index fb8b449a7..d0a3e0d08 100644
--- a/src/ARMeilleure/CodeGen/Linking/RelocInfo.cs
+++ b/src/ARMeilleure/CodeGen/Linking/RelocInfo.cs
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
///
/// Represents relocation information about a .
///
- readonly struct RelocInfo
+ public readonly struct RelocInfo
{
///
/// Gets an empty .
diff --git a/src/ARMeilleure/CodeGen/Linking/Symbol.cs b/src/ARMeilleure/CodeGen/Linking/Symbol.cs
index 5559afe09..6da5a2765 100644
--- a/src/ARMeilleure/CodeGen/Linking/Symbol.cs
+++ b/src/ARMeilleure/CodeGen/Linking/Symbol.cs
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
///
/// Represents a symbol.
///
- readonly struct Symbol
+ public readonly struct Symbol
{
private readonly ulong _value;
diff --git a/src/ARMeilleure/CodeGen/Linking/SymbolType.cs b/src/ARMeilleure/CodeGen/Linking/SymbolType.cs
index 29011a762..3232502ea 100644
--- a/src/ARMeilleure/CodeGen/Linking/SymbolType.cs
+++ b/src/ARMeilleure/CodeGen/Linking/SymbolType.cs
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
///
/// Types of .
///
- enum SymbolType : byte
+ public enum SymbolType : byte
{
///
/// Refers to nothing, i.e no symbol.
diff --git a/src/ARMeilleure/CodeGen/Unwinding/UnwindInfo.cs b/src/ARMeilleure/CodeGen/Unwinding/UnwindInfo.cs
index 127b84231..6be1f5d6f 100644
--- a/src/ARMeilleure/CodeGen/Unwinding/UnwindInfo.cs
+++ b/src/ARMeilleure/CodeGen/Unwinding/UnwindInfo.cs
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
- struct UnwindInfo
+ public struct UnwindInfo
{
public const int Stride = 4; // Bytes.
diff --git a/src/ARMeilleure/CodeGen/Unwinding/UnwindPseudoOp.cs b/src/ARMeilleure/CodeGen/Unwinding/UnwindPseudoOp.cs
index 2045019a3..6c4021d45 100644
--- a/src/ARMeilleure/CodeGen/Unwinding/UnwindPseudoOp.cs
+++ b/src/ARMeilleure/CodeGen/Unwinding/UnwindPseudoOp.cs
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
- enum UnwindPseudoOp
+ public enum UnwindPseudoOp
{
PushReg = 0,
SetFrame = 1,
diff --git a/src/ARMeilleure/CodeGen/Unwinding/UnwindPushEntry.cs b/src/ARMeilleure/CodeGen/Unwinding/UnwindPushEntry.cs
index 507ace598..5bce9482a 100644
--- a/src/ARMeilleure/CodeGen/Unwinding/UnwindPushEntry.cs
+++ b/src/ARMeilleure/CodeGen/Unwinding/UnwindPushEntry.cs
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
- struct UnwindPushEntry
+ public struct UnwindPushEntry
{
public const int Stride = 16; // Bytes.
diff --git a/src/ARMeilleure/Common/AddressTableLevel.cs b/src/ARMeilleure/Common/AddressTableLevel.cs
index af3b9b99f..58cfe21ed 100644
--- a/src/ARMeilleure/Common/AddressTableLevel.cs
+++ b/src/ARMeilleure/Common/AddressTableLevel.cs
@@ -6,7 +6,7 @@ namespace ARMeilleure.Common
public readonly struct AddressTableLevel
{
///
- /// Gets the index of the in the guest address.
+ /// Gets the index of the in the guest address.
///
public int Index { get; }
diff --git a/src/ARMeilleure/Memory/ReservedRegion.cs b/src/ARMeilleure/Memory/ReservedRegion.cs
index dfe17c933..5b0a95f67 100644
--- a/src/ARMeilleure/Memory/ReservedRegion.cs
+++ b/src/ARMeilleure/Memory/ReservedRegion.cs
@@ -2,7 +2,7 @@ using System;
namespace ARMeilleure.Memory
{
- public class ReservedRegion
+ public class ReservedRegion : IDisposable
{
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
diff --git a/src/ARMeilleure/Signal/TestMethods.cs b/src/ARMeilleure/Signal/TestMethods.cs
index 684157860..013705f21 100644
--- a/src/ARMeilleure/Signal/TestMethods.cs
+++ b/src/ARMeilleure/Signal/TestMethods.cs
@@ -1,5 +1,6 @@
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
+using ARMeilleure.Translation.Cache;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
@@ -17,7 +18,7 @@ namespace ARMeilleure.Signal
public delegate int DebugThreadLocalMapGetOrReserve(int threadId, int initialState);
public delegate void DebugNativeWriteLoop(nint nativeWriteLoopPtr, nint writePtr);
- public static DebugPartialUnmap GenerateDebugPartialUnmap()
+ public static DebugPartialUnmap GenerateDebugPartialUnmap(JitCache jitCache)
{
EmitterContext context = new();
@@ -31,10 +32,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
- return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(jitCache);
}
- public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(nint structPtr)
+ public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(JitCache jitCache, nint structPtr)
{
EmitterContext context = new();
@@ -48,10 +49,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
- return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(jitCache);
}
- public static DebugNativeWriteLoop GenerateDebugNativeWriteLoop()
+ public static DebugNativeWriteLoop GenerateDebugNativeWriteLoop(JitCache jitCache)
{
EmitterContext context = new();
@@ -77,7 +78,7 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
- return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(jitCache);
}
}
}
diff --git a/src/ARMeilleure/Translation/Cache/CacheEntry.cs b/src/ARMeilleure/Translation/Cache/CacheEntry.cs
index 25b06f781..dc356cb72 100644
--- a/src/ARMeilleure/Translation/Cache/CacheEntry.cs
+++ b/src/ARMeilleure/Translation/Cache/CacheEntry.cs
@@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace ARMeilleure.Translation.Cache
{
- readonly struct CacheEntry : IComparable
+ public readonly struct CacheEntry : IComparable
{
public int Offset { get; }
public int Size { get; }
diff --git a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs
index 9c5ca29df..162859179 100644
--- a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs
+++ b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs
@@ -22,12 +22,31 @@ namespace ARMeilleure.Translation.Cache
return Offset.CompareTo(other.Offset);
}
}
+
+ private readonly int _regionSize;
+ private int _regionCount;
private readonly List _blocks = [];
- public CacheMemoryAllocator(int capacity)
+ public CacheMemoryAllocator(int regionSize, int initialRegionCount = 1)
{
- _blocks.Add(new MemoryBlock(0, capacity));
+ _regionCount = 0;
+ _regionSize = regionSize;
+
+ for (; initialRegionCount > 0; initialRegionCount--)
+ {
+ _blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
+ _regionCount++;
+ }
+ }
+
+ public void AddNewBlocks(int count)
+ {
+ for (; count > 0; count--)
+ {
+ _blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
+ _regionCount++;
+ }
}
public int Allocate(int size)
@@ -65,13 +84,14 @@ namespace ARMeilleure.Translation.Cache
{
index = ~index;
}
+
+ int endOffs = block.Offset + block.Size;
- if (index < _blocks.Count)
+ // Don't merge blocks from different allocations
+ if (index < _blocks.Count && endOffs % _regionSize != 0)
{
MemoryBlock next = _blocks[index];
- int endOffs = block.Offset + block.Size;
-
if (next.Offset == endOffs)
{
block = new MemoryBlock(block.Offset, block.Size + next.Size);
@@ -79,7 +99,8 @@ namespace ARMeilleure.Translation.Cache
}
}
- if (index > 0)
+ // Don't merge blocks from different allocations
+ if (index > 0 && block.Offset % _regionSize != 0)
{
MemoryBlock prev = _blocks[index - 1];
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index 7931fb360..0fae275c7 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -14,62 +14,35 @@ using System.Threading;
namespace ARMeilleure.Translation.Cache
{
- static partial class JitCache
+ public partial class JitCache : IDisposable
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
- private const int CacheSize = 256 * 1024 * 1024;
+ private const uint CacheSize = 256 * 1024 * 1024;
- private static JitCacheInvalidation _jitCacheInvalidator;
+ private readonly JitCacheInvalidation _jitCacheInvalidator;
- private static readonly List _cacheAllocators = [];
+ private readonly CacheMemoryAllocator _cacheAllocator;
- private static readonly List _cacheEntries = [];
+ private readonly List _cacheEntries = [];
- private static readonly Lock _lock = new();
- private static bool _initialized;
+ private readonly Lock _lock = new();
- private static readonly List _jitRegions = [];
- private static int _activeRegionIndex = 0;
+ private readonly List _jitRegions = [];
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
- public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
+ private static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
- public static void Initialize(IJitMemoryAllocator allocator)
+ public JitCache(IJitMemoryAllocator allocator)
{
lock (_lock)
{
- if (_initialized)
- {
- if (OperatingSystem.IsWindows())
- {
- JitUnwindWindows.RemoveFunctionTableHandler(
- _jitRegions[0].Pointer);
- }
+ _jitRegions.Add(new(allocator, CacheSize));
- for (int i = 0; i < _jitRegions.Count; i++)
- {
- _jitRegions[i].Dispose();
- }
-
- _jitRegions.Clear();
- _cacheAllocators.Clear();
- }
- else
- {
- _initialized = true;
- }
-
- _activeRegionIndex = 0;
-
- ReservedRegion firstRegion = new(allocator, CacheSize);
- _jitRegions.Add(firstRegion);
-
- CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
- _cacheAllocators.Add(firstCacheAllocator);
+ _cacheAllocator = new((int)CacheSize);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -79,23 +52,20 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(
- firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
+ this, _jitRegions[0].Pointer, CacheSize, _jitRegions[0].Pointer + Allocate(_pageSize)
);
}
}
}
- public static nint Map(CompiledFunction func)
+ public nint Map(CompiledFunction func)
{
byte[] code = func.Code;
lock (_lock)
{
- Debug.Assert(_initialized);
-
int funcOffset = Allocate(code.Length);
- ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
- nint funcPtr = targetRegion.Pointer + funcOffset;
+ nint funcPtr = GetFunctionPtr(funcOffset);
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -109,9 +79,9 @@ namespace ARMeilleure.Translation.Cache
}
else
{
- ReprotectAsWritable(targetRegion, funcOffset, code.Length);
+ ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length);
- ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
+ ReprotectAsExecutable(funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -129,25 +99,24 @@ namespace ARMeilleure.Translation.Cache
}
}
- public static void Unmap(nint pointer)
+ public void Unmap(nint pointer)
{
lock (_lock)
{
- Debug.Assert(_initialized);
-
- foreach (ReservedRegion region in _jitRegions)
+ for (int i = 0; i < _jitRegions.Count; i++)
{
- if (pointer.ToInt64() < region.Pointer.ToInt64() ||
- pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
+ ReservedRegion jitRegion = _jitRegions[i];
+ if (pointer.ToInt64() < jitRegion.Pointer.ToInt64() ||
+ pointer.ToInt64() >= (jitRegion.Pointer + (nint)CacheSize).ToInt64())
{
continue;
}
- int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
+ int funcOffset = (int)(pointer.ToInt64() - jitRegion.Pointer.ToInt64() + i * CacheSize);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
- _cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
+ _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
@@ -156,53 +125,63 @@ namespace ARMeilleure.Translation.Cache
}
}
- private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
+ private void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
- int regionStart = offset & ~_pageMask;
- int regionEnd = (endOffs + _pageMask) & ~_pageMask;
-
- region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
+ int regionStart = (offset % (int)CacheSize) & ~_pageMask;
+ int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
+
+ GetRegion(offset).Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
- private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
+ private void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
- int regionStart = offset & ~_pageMask;
- int regionEnd = (endOffs + _pageMask) & ~_pageMask;
+ int regionStart = (offset % (int)CacheSize) & ~_pageMask;
+ int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
- region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
+ GetRegion(offset).Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
- private static int Allocate(int codeSize)
+ private int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
- int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(codeSize);
+ int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
- _jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
+ GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
- int exhaustedRegion = _activeRegionIndex;
+ _cacheAllocator.AddNewBlocks(1);
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
+
+ Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache of size {(_jitRegions.Count * CacheSize).Bytes()} exhausted, creating new Cache Region ({((_jitRegions.Count + 1) * CacheSize).Bytes()} Total Allocation).");
+
_jitRegions.Add(newRegion);
- _activeRegionIndex = _jitRegions.Count - 1;
-
- Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize).Bytes()} Total Allocation).");
-
- _cacheAllocators.Add(new CacheMemoryAllocator(CacheSize));
-
- int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(codeSize);
- if (allocOffsetNew < 0)
+
+ allocOffset = _cacheAllocator.Allocate(codeSize);
+ if (allocOffset < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
- newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
- return allocOffsetNew;
+ GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
+ return allocOffset;
+ }
+
+ private nint GetFunctionPtr(int offset)
+ {
+ return GetRegion(offset).Pointer + (offset % (int)CacheSize);
+ }
+
+ private ReservedRegion GetRegion(int offset)
+ {
+ int index = offset / (int)CacheSize;
+
+ return _jitRegions[index];
}
private static int AlignCodeSize(int codeSize)
@@ -210,7 +189,7 @@ namespace ARMeilleure.Translation.Cache
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
}
- private static void Add(int offset, int size, UnwindInfo unwindInfo)
+ private void Add(int offset, int size, UnwindInfo unwindInfo)
{
CacheEntry entry = new(offset, size, unwindInfo);
@@ -224,25 +203,22 @@ namespace ARMeilleure.Translation.Cache
_cacheEntries.Insert(index, entry);
}
- public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
+ public bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
- foreach (ReservedRegion _ in _jitRegions)
+ int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
+
+ if (index < 0)
{
- int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
+ index = ~index - 1;
+ }
- if (index < 0)
- {
- index = ~index - 1;
- }
-
- if (index >= 0)
- {
- entry = _cacheEntries[index];
- entryIndex = index;
- return true;
- }
+ if (index >= 0)
+ {
+ entry = _cacheEntries[index];
+ entryIndex = index;
+ return true;
}
}
@@ -250,5 +226,18 @@ namespace ARMeilleure.Translation.Cache
entryIndex = 0;
return false;
}
+
+ public void Dispose()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ JitUnwindWindows.RemoveFunctionTableHandler(_jitRegions[0].Pointer);
+ }
+
+ foreach (ReservedRegion jitRegion in _jitRegions)
+ {
+ jitRegion.Dispose();
+ }
+ }
}
}
diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
index 15a1051fa..64129d811 100644
--- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
+++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
@@ -2,6 +2,8 @@
using ARMeilleure.CodeGen.Unwinding;
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
@@ -26,6 +28,29 @@ namespace ARMeilleure.Translation.Cache
public byte FrameRegister;
public unsafe fixed ushort UnwindCodes[MaxUnwindCodesArraySize];
}
+
+ private unsafe struct InternalFunctionHandler
+ {
+ public InternalFunctionHandler(JitCache jitCache, nint workBufferPtr)
+ {
+ _jitCache = jitCache;
+
+ _runtimeFunction = (RuntimeFunction*)workBufferPtr;
+
+ _unwindInfo = (UnwindInfo*)(workBufferPtr + _sizeOfRuntimeFunction);
+ }
+
+ readonly JitCache _jitCache;
+
+ readonly RuntimeFunction* _runtimeFunction;
+
+ readonly UnwindInfo* _unwindInfo;
+
+ public RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context)
+ {
+ return JitUnwindWindows.FunctionTableHandler(_jitCache, _runtimeFunction, _unwindInfo, controlPc, context);
+ }
+ }
private enum UnwindOp
{
@@ -59,27 +84,28 @@ namespace ARMeilleure.Translation.Cache
private static GetRuntimeFunctionCallback _getRuntimeFunctionCallback;
- private static int _sizeOfRuntimeFunction;
+ private static readonly int _sizeOfRuntimeFunction;
+
+ private static readonly ConcurrentDictionary _functionTableHandlers = new();
- private unsafe static RuntimeFunction* _runtimeFunction;
+ static JitUnwindWindows()
+ {
+ _sizeOfRuntimeFunction = Marshal.SizeOf();
+ }
- private unsafe static UnwindInfo* _unwindInfo;
-
- public static void InstallFunctionTableHandler(nint codeCachePointer, uint codeCacheLength, nint workBufferPtr)
+ public static void InstallFunctionTableHandler(JitCache jitCache, nint codeCachePointer, uint codeCacheLength, nint workBufferPtr)
{
ulong codeCachePtr = (ulong)codeCachePointer.ToInt64();
- _sizeOfRuntimeFunction = Marshal.SizeOf();
-
bool result;
+ InternalFunctionHandler handler;
+
unsafe
{
- _runtimeFunction = (RuntimeFunction*)workBufferPtr;
+ handler = new InternalFunctionHandler(jitCache, workBufferPtr);
- _unwindInfo = (UnwindInfo*)(workBufferPtr + _sizeOfRuntimeFunction);
-
- _getRuntimeFunctionCallback = new GetRuntimeFunctionCallback(FunctionTableHandler);
+ _getRuntimeFunctionCallback = handler.FunctionTableHandler;
result = RtlInstallFunctionTableCallback(
codeCachePtr | 3,
@@ -94,6 +120,8 @@ namespace ARMeilleure.Translation.Cache
{
throw new InvalidOperationException("Failure installing function table callback.");
}
+
+ _functionTableHandlers.TryAdd(codeCachePtr, handler);
}
public static void RemoveFunctionTableHandler(nint codeCachePointer)
@@ -111,24 +139,26 @@ namespace ARMeilleure.Translation.Cache
{
throw new InvalidOperationException("Failure removing function table callback.");
}
+
+ _functionTableHandlers.Remove(codeCachePtr, out _);
}
- private static unsafe RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context)
+ private static unsafe RuntimeFunction* FunctionTableHandler(JitCache jitCache, RuntimeFunction* runtimeFunction, UnwindInfo* unwindInfo, ulong controlPc, nint context)
{
int offset = (int)((long)controlPc - context.ToInt64());
- if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _))
+ if (!jitCache.TryFind(offset, out CacheEntry funcEntry, out _))
{
return null; // Not found.
}
- CodeGen.Unwinding.UnwindInfo unwindInfo = funcEntry.UnwindInfo;
+ CodeGen.Unwinding.UnwindInfo funcUnwindInfo = funcEntry.UnwindInfo;
int codeIndex = 0;
- for (int index = unwindInfo.PushEntries.Length - 1; index >= 0; index--)
+ for (int index = funcUnwindInfo.PushEntries.Length - 1; index >= 0; index--)
{
- UnwindPushEntry entry = unwindInfo.PushEntries[index];
+ UnwindPushEntry entry = funcUnwindInfo.PushEntries[index];
switch (entry.PseudoOp)
{
@@ -140,14 +170,14 @@ namespace ARMeilleure.Translation.Cache
if (stackOffset <= 0xFFFF0)
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128, entry.PrologOffset, entry.RegIndex);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset / 16);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128, entry.PrologOffset, entry.RegIndex);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset / 16);
}
else
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128Far, entry.PrologOffset, entry.RegIndex);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 0);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 16);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128Far, entry.PrologOffset, entry.RegIndex);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 0);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 16);
}
break;
@@ -161,18 +191,18 @@ namespace ARMeilleure.Translation.Cache
if (allocSize <= 128)
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocSmall, entry.PrologOffset, (allocSize / 8) - 1);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocSmall, entry.PrologOffset, (allocSize / 8) - 1);
}
else if (allocSize <= 0x7FFF8)
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 0);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize / 8);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 0);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize / 8);
}
else
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 1);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 0);
- _unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 16);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 1);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 0);
+ unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 16);
}
break;
@@ -180,7 +210,7 @@ namespace ARMeilleure.Translation.Cache
case UnwindPseudoOp.PushReg:
{
- _unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.PushNonvol, entry.PrologOffset, entry.RegIndex);
+ unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.PushNonvol, entry.PrologOffset, entry.RegIndex);
break;
}
@@ -192,16 +222,16 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(codeIndex <= MaxUnwindCodesArraySize);
- _unwindInfo->VersionAndFlags = 1; // Flags: The function has no handler.
- _unwindInfo->SizeOfProlog = (byte)unwindInfo.PrologSize;
- _unwindInfo->CountOfUnwindCodes = (byte)codeIndex;
- _unwindInfo->FrameRegister = 0;
+ unwindInfo->VersionAndFlags = 1; // Flags: The function has no handler.
+ unwindInfo->SizeOfProlog = (byte)funcUnwindInfo.PrologSize;
+ unwindInfo->CountOfUnwindCodes = (byte)codeIndex;
+ unwindInfo->FrameRegister = 0;
- _runtimeFunction->BeginAddress = (uint)funcEntry.Offset;
- _runtimeFunction->EndAddress = (uint)(funcEntry.Offset + funcEntry.Size);
- _runtimeFunction->UnwindData = (uint)_sizeOfRuntimeFunction;
+ runtimeFunction->BeginAddress = (uint)funcEntry.Offset;
+ runtimeFunction->EndAddress = (uint)(funcEntry.Offset + funcEntry.Size);
+ runtimeFunction->UnwindData = (uint)_sizeOfRuntimeFunction;
- return _runtimeFunction;
+ return runtimeFunction;
}
private static ushort PackUnwindOp(UnwindOp op, int prologOffset, int opInfo)
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 5534cde56..9ce3e5773 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -4,6 +4,7 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.State;
+using ARMeilleure.Translation.Cache;
using Humanizer;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@@ -33,7 +34,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
- private const uint InternalVersion = 7010; //! To be incremented manually for each change to the ARMeilleure project.
+ private const uint InternalVersion = 7016; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@@ -50,6 +51,8 @@ namespace ARMeilleure.Translation.PTC
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
public PtcProfiler Profiler { get; }
+
+ private readonly JitCache _jitCache;
// Carriers.
private MemoryStream _infosStream;
@@ -81,7 +84,7 @@ namespace ARMeilleure.Translation.PTC
private volatile int _translateTotalCount;
public event Action PtcStateChanged;
- public Ptc()
+ public Ptc(JitCache jitCache)
{
Profiler = new PtcProfiler(this);
@@ -92,6 +95,8 @@ namespace ARMeilleure.Translation.PTC
_waitEvent = new ManualResetEvent(true);
+ _jitCache = jitCache;
+
_disposed = false;
TitleIdText = TitleIdTextDefault;
@@ -782,7 +787,7 @@ namespace ARMeilleure.Translation.PTC
return new UnwindInfo(pushEntries, prologueSize);
}
- private static TranslatedFunction FastTranslate(
+ private TranslatedFunction FastTranslate(
byte[] code,
Counter callCounter,
ulong guestSize,
@@ -790,7 +795,7 @@ namespace ARMeilleure.Translation.PTC
bool highCq)
{
CompiledFunction cFunc = new(code, unwindInfo, RelocInfo.Empty);
- GuestFunction gFunc = cFunc.MapWithPointer(out nint gFuncPointer);
+ GuestFunction gFunc = cFunc.MapWithPointer(_jitCache, out nint gFuncPointer);
return new TranslatedFunction(gFunc, gFuncPointer, callCounter, guestSize, highCq);
}
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index b098ff4cf..e73ecc85e 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -24,6 +24,7 @@ namespace ARMeilleure.Translation
private readonly IJitMemoryAllocator _allocator;
private readonly ConcurrentQueue> _oldFuncs;
+ public readonly JitCache JitCache;
private readonly Ptc _ptc;
internal TranslatorCache Functions { get; }
@@ -43,16 +44,17 @@ namespace ARMeilleure.Translation
_oldFuncs = new ConcurrentQueue>();
- _ptc = new Ptc();
+ JitCache = new JitCache(allocator);
+
+ _ptc = new Ptc(JitCache);
Queue = new TranslatorQueue();
- JitCache.Initialize(allocator);
CountTable = new EntryTable();
Functions = new TranslatorCache();
FunctionTable = functionTable;
- Stubs = new TranslatorStubs(FunctionTable);
+ Stubs = new TranslatorStubs(JitCache, FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
}
@@ -167,6 +169,7 @@ namespace ARMeilleure.Translation
}
ClearJitCache();
+ JitCache.Dispose();
Stubs.Dispose();
FunctionTable.Dispose();
@@ -305,7 +308,7 @@ namespace ARMeilleure.Translation
_ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
}
- GuestFunction func = compiledFunc.MapWithPointer(out nint funcPointer);
+ GuestFunction func = compiledFunc.MapWithPointer(JitCache, out nint funcPointer);
Allocators.ResetAll();
diff --git a/src/ARMeilleure/Translation/TranslatorStubs.cs b/src/ARMeilleure/Translation/TranslatorStubs.cs
index 2d95ceb99..92fb2d2b7 100644
--- a/src/ARMeilleure/Translation/TranslatorStubs.cs
+++ b/src/ARMeilleure/Translation/TranslatorStubs.cs
@@ -14,6 +14,8 @@ namespace ARMeilleure.Translation
///
class TranslatorStubs : IDisposable
{
+ private readonly JitCache _jitCache;
+
private readonly Lazy _slowDispatchStub;
private bool _disposed;
@@ -83,11 +85,14 @@ namespace ARMeilleure.Translation
/// Initializes a new instance of the class with the specified
/// instance.
///
+ /// Jit cache to map functions in
/// Function table used to store pointers to the functions that the guest code will call
/// is null
- public TranslatorStubs(IAddressTable functionTable)
+ public TranslatorStubs(JitCache jitCache, IAddressTable functionTable)
{
ArgumentNullException.ThrowIfNull(functionTable);
+
+ _jitCache = jitCache;
_functionTable = functionTable;
_slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
@@ -115,12 +120,12 @@ namespace ARMeilleure.Translation
{
if (_dispatchStub.IsValueCreated)
{
- JitCache.Unmap(_dispatchStub.Value);
+ _jitCache.Unmap(_dispatchStub.Value);
}
if (_dispatchLoop.IsValueCreated)
{
- JitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
+ _jitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
}
_disposed = true;
@@ -188,7 +193,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64];
- GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(_jitCache);
return Marshal.GetFunctionPointerForDelegate(func);
}
@@ -213,7 +218,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64];
- GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(_jitCache);
return Marshal.GetFunctionPointerForDelegate(func);
}
@@ -290,7 +295,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.None;
OperandType[] argTypes = [OperandType.I64, OperandType.I64];
- return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(_jitCache);
}
///
@@ -314,7 +319,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64, OperandType.I64];
- return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(_jitCache);
}
}
}
diff --git a/src/ARMeilleure/Translation/TranslatorTestMethods.cs b/src/ARMeilleure/Translation/TranslatorTestMethods.cs
index 5f15cf550..a4ad62dd2 100644
--- a/src/ARMeilleure/Translation/TranslatorTestMethods.cs
+++ b/src/ARMeilleure/Translation/TranslatorTestMethods.cs
@@ -1,6 +1,7 @@
using ARMeilleure.CodeGen.X86;
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.State;
+using ARMeilleure.Translation.Cache;
using System;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
@@ -59,7 +60,7 @@ namespace ARMeilleure.Translation
return context.VectorExtract(OperandType.I32, vec, 0);
}
- public static FpFlagsPInvokeTest GenerateFpFlagsPInvokeTest()
+ public static FpFlagsPInvokeTest GenerateFpFlagsPInvokeTest(JitCache jitCache)
{
EmitterContext context = new();
@@ -141,7 +142,7 @@ namespace ARMeilleure.Translation
OperandType[] argTypes = [OperandType.I64];
- return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map();
+ return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map(jitCache);
}
}
}
diff --git a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs
index 24a318be4..65808174f 100644
--- a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs
+++ b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs
@@ -128,7 +128,7 @@ namespace Ryujinx.BuildValidationTasks
using (StreamWriter sw = new(path))
{
- sw.Write(jsonString);
+ sw.Write($"{jsonString}\n");
}
}
diff --git a/src/Ryujinx.Common/Configuration/Hid/AssignedInputDevice.cs b/src/Ryujinx.Common/Configuration/Hid/AssignedInputDevice.cs
new file mode 100644
index 000000000..ee7add6a4
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/AssignedInputDevice.cs
@@ -0,0 +1,11 @@
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class AssignedInputDevice
+ {
+ public AssignedInputDeviceType Type { get; set; }
+
+ public string Id { get; set; }
+
+ public string ProfileName { get; set; }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/AssignedInputDeviceType.cs b/src/Ryujinx.Common/Configuration/Hid/AssignedInputDeviceType.cs
new file mode 100644
index 000000000..aff9971ef
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/AssignedInputDeviceType.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum AssignedInputDeviceType
+ {
+ Keyboard,
+ Controller,
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs
index ccf9ead16..874961c70 100644
--- a/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/InputConfig.cs
@@ -36,6 +36,12 @@ namespace Ryujinx.Common.Configuration.Hid
///
public PlayerIndex PlayerIndex { get; set; }
+ ///
+ /// Allow a keyboard configuration to temporarily promote to a connected gamepad,
+ /// while preserving the existing keyboard fallback path when that gamepad disappears.
+ ///
+ public bool EnableDynamicGamepadSwap { get; set; }
+
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
diff --git a/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignment.cs b/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignment.cs
new file mode 100644
index 000000000..aacd8400c
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignment.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public class PlayerInputAssignment
+ {
+ public PlayerIndex PlayerIndex { get; set; }
+
+ public bool EnableDynamicInputSwap { get; set; }
+
+ public List Devices { get; set; } = [];
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignmentHelper.cs b/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignmentHelper.cs
new file mode 100644
index 000000000..5bfa26a45
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/PlayerInputAssignmentHelper.cs
@@ -0,0 +1,166 @@
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Common.Configuration.Hid
+{
+ public static class PlayerInputAssignmentHelper
+ {
+ public static AssignedInputDevice CreatePrimaryDevice(InputConfig inputConfig)
+ {
+ if (inputConfig == null || string.IsNullOrWhiteSpace(inputConfig.Id))
+ {
+ return null;
+ }
+
+ return new AssignedInputDevice
+ {
+ Type = inputConfig is StandardKeyboardInputConfig
+ ? AssignedInputDeviceType.Keyboard
+ : AssignedInputDeviceType.Controller,
+ Id = inputConfig.Id,
+ };
+ }
+
+ public static PlayerInputAssignment Normalize(PlayerInputAssignment assignment, AssignedInputDevice preferredDevice = null)
+ {
+ if (assignment == null)
+ {
+ return null;
+ }
+
+ PlayerInputAssignment normalized = new()
+ {
+ PlayerIndex = assignment.PlayerIndex,
+ EnableDynamicInputSwap = assignment.EnableDynamicInputSwap,
+ };
+
+ List distinctDevices = Deduplicate(assignment.Devices);
+
+ if (assignment.EnableDynamicInputSwap)
+ {
+ normalized.Devices.AddRange(distinctDevices.Select(Clone));
+ return normalized;
+ }
+
+ AssignedInputDevice primaryDevice = SelectPrimaryDevice(distinctDevices, preferredDevice) ?? Clone(preferredDevice);
+
+ if (primaryDevice != null)
+ {
+ normalized.Devices.Add(Clone(primaryDevice));
+ }
+
+ return normalized;
+ }
+
+ public static bool AreEquivalent(
+ PlayerInputAssignment left,
+ PlayerInputAssignment right,
+ AssignedInputDevice leftPreferredDevice = null,
+ AssignedInputDevice rightPreferredDevice = null)
+ {
+ if (left == null || right == null)
+ {
+ return left == right;
+ }
+
+ PlayerInputAssignment normalizedLeft = Normalize(left, leftPreferredDevice);
+ PlayerInputAssignment normalizedRight = Normalize(right, rightPreferredDevice);
+
+ if (normalizedLeft.EnableDynamicInputSwap != normalizedRight.EnableDynamicInputSwap)
+ {
+ return false;
+ }
+
+ List<(AssignedInputDeviceType Type, string Id, string ProfileName)> leftDevices = normalizedLeft.Devices
+ .Select(device => (Type: device.Type, Id: device.Id, ProfileName: device.ProfileName ?? string.Empty))
+ .OrderBy(device => device.Type)
+ .ThenBy(device => device.Id, StringComparer.Ordinal)
+ .ThenBy(device => device.ProfileName, StringComparer.Ordinal)
+ .ToList();
+
+ List<(AssignedInputDeviceType Type, string Id, string ProfileName)> rightDevices = normalizedRight.Devices
+ .Select(device => (Type: device.Type, Id: device.Id, ProfileName: device.ProfileName ?? string.Empty))
+ .OrderBy(device => device.Type)
+ .ThenBy(device => device.Id, StringComparer.Ordinal)
+ .ThenBy(device => device.ProfileName, StringComparer.Ordinal)
+ .ToList();
+
+ return leftDevices.SequenceEqual(rightDevices);
+ }
+
+ private static List Deduplicate(IEnumerable devices)
+ {
+ List result = [];
+
+ if (devices == null)
+ {
+ return result;
+ }
+
+ foreach (AssignedInputDevice device in devices)
+ {
+ if (device == null || string.IsNullOrWhiteSpace(device.Id))
+ {
+ continue;
+ }
+
+ int existingIndex = result.FindIndex(existing =>
+ existing.Type == device.Type &&
+ string.Equals(existing.Id, device.Id, StringComparison.Ordinal));
+
+ if (existingIndex == -1)
+ {
+ result.Add(Clone(device));
+ continue;
+ }
+
+ if (!string.IsNullOrWhiteSpace(device.ProfileName) ||
+ string.IsNullOrWhiteSpace(result[existingIndex].ProfileName))
+ {
+ result[existingIndex].ProfileName = device.ProfileName;
+ }
+ }
+
+ return result;
+ }
+
+ private static AssignedInputDevice SelectPrimaryDevice(List devices, AssignedInputDevice preferredDevice)
+ {
+ if (devices == null || devices.Count == 0)
+ {
+ return null;
+ }
+
+ if (preferredDevice != null)
+ {
+ AssignedInputDevice matchedDevice = devices.FirstOrDefault(device =>
+ device.Type == preferredDevice.Type &&
+ string.Equals(device.Id, preferredDevice.Id, StringComparison.Ordinal));
+
+ if (matchedDevice != null)
+ {
+ return matchedDevice;
+ }
+ }
+
+ return devices[0];
+ }
+
+ private static AssignedInputDevice Clone(AssignedInputDevice device)
+ {
+ if (device == null)
+ {
+ return null;
+ }
+
+ return new AssignedInputDevice
+ {
+ Type = device.Type,
+ Id = device.Id,
+ ProfileName = device.ProfileName,
+ };
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs
index 24a6e364a..fe4ce9b67 100644
--- a/src/Ryujinx.Common/TitleIDs.cs
+++ b/src/Ryujinx.Common/TitleIDs.cs
@@ -17,6 +17,7 @@ namespace Ryujinx.Common
//Dragon Quest Franchise
"010008900705c000", // Dragon Quest Builders
"010042000a986000", // Dragon Quest Builders 2
+ "010021701c45e000", // Dragon Quest VII Reimagined Demo
//Fire Emblem Franchise
"0100a6301214e000", // Fire Emblem Engage
@@ -33,7 +34,6 @@ namespace Ryujinx.Common
"01007e3006dda000", // Kirby Star Allies
"01003fb00c5a8000", // Super Kirby Clash
-
//The Zelda Franchise
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
@@ -43,8 +43,7 @@ namespace Ryujinx.Common
"01002da013484000", // The Legend of Zelda: Skyward Sword HD
"0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom
"01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom
-
-
+
//Luigi Franchise
"010048701995e000", // Luigi's Mansion 2 HD
"0100dca0064a6000", // Luigi's Mansion 3
@@ -75,16 +74,13 @@ namespace Ryujinx.Common
"010049900f546002", // Super Mario 3D All-Stars | Super Mario Sunshine
"010049900f546003", // Super Mario 3D All-Stars | Super Mario Galaxy
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
- "010049900F546001", // Super Mario 64
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"010015100b514000", // Super Mario Bros. Wonder
- "010049900F546003", // Super Mario Galaxy
"01009b90006dc000", // Super Mario Maker 2
"0100000000010000", // SUPER MARIO ODYSSEY
"010036b0034e4000", // Super Mario Party
"0100965017338000", // Super Mario Party Jamboree
"0100bc0018138000", // Super Mario RPG
- "010049900F546002", // Super Mario Sunshine
"0100a3900c3e2000", // Paper Mario: The Origami King
"0100ecd018ebe000", // Paper Mario: The Thousand-Year Door
"01009cC022b96000", // Super Mario Galaxy
@@ -149,6 +145,7 @@ namespace Ryujinx.Common
"010067b017588000", // Endless Ocean Luminous
"01003da010e8a000", // Miitopia
"01006b5012b32000", // Part Time UFO
+ "0100107027310000", // Rhythm Heave Groove: Starter Demo
"0100704000B3A000", // Snipperclips
"01006a800016e000", // Super Smash Bros. Ultimate
"0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore
@@ -176,7 +173,7 @@ namespace Ryujinx.Common
"01004ad014bf0000", // Sonic Frontiers
"01009aa000faa000", // Sonic Mania
"01005ea01c0fc000", // SONIC X SHADOW GENERATIONS
- "01005ea01c0fc001", // ^
+ "01005ea01c0fc001", // ^ TODO: (Needs to be differentiated like 3D All Stars)
"01006e001823c000", // Sonic Racing: CrossWorlds
"010064b0242be000", // Sonic Racing: CrossWorlds - Demo
@@ -199,6 +196,7 @@ namespace Ryujinx.Common
"0100744001588000", // Cars 3: Driven to Win
"0100b41013c82000", // Cruis'n Blast
"010085900337e000", // Death Squared
+ "0100a0d022a68000", // DELTARUNE
"01001b300b9be000", // Diablo III: Eternal Collection
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
"01008c8012920000", // Dying Light Platinum Edition
@@ -211,6 +209,8 @@ namespace Ryujinx.Common
"010013c00e930000", // Hollow Knight: Silksong
"010085500130a000", // Lego City: Undercover
"010073c01af34000", // LEGO Horizon Adventures
+ "0100653026dde000", // MARVEL Cosmic Invasion Demo
+ "0100419014456000", // Mary Skelter Finale
"0100d71004694000", // Minecraft
"01007430037f6000", // Monopoly
"0100853015e86000", // No Man's Sky
@@ -231,6 +231,8 @@ namespace Ryujinx.Common
"01008d100d43e000", // Saints Row IV
"0100de600beee000", // Saints Row: The Third - The Full Package
"01001180021fa000", // Shovel Knight: Specter of Torment
+ "0100ccc0002e6000", // Skylanders Imaginators (US)
+ "010048d000bfc000", // Skylanders Imaginators (EU)
"010079f00671c000", // Sparkle 2: Evo
"010077b00e046000", // Spyro: Reignited Trilogy
"0100e1D01eb2e000", // Squeakross: Home Squeak Home
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/CacheMemoryAllocator.cs b/src/Ryujinx.Cpu/LightningJit/Cache/CacheMemoryAllocator.cs
index 05c889922..519fa54a0 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/CacheMemoryAllocator.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/CacheMemoryAllocator.cs
@@ -23,12 +23,31 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return Offset.CompareTo(other.Offset);
}
}
+
+ private readonly int _regionSize;
+ private int _regionCount;
private readonly List _blocks = [];
- public CacheMemoryAllocator(int capacity)
+ public CacheMemoryAllocator(int regionSize, int initialRegionCount = 1)
{
- _blocks.Add(new MemoryBlock(0, capacity));
+ _regionCount = 0;
+ _regionSize = regionSize;
+
+ for (; initialRegionCount > 0; initialRegionCount--)
+ {
+ _blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
+ _regionCount++;
+ }
+ }
+
+ public void AddNewBlocks(int count)
+ {
+ for (; count > 0; count--)
+ {
+ _blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
+ _regionCount++;
+ }
}
public int Allocate(int size)
@@ -100,13 +119,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
index = ~index;
}
+
+ int endOffs = block.Offset + block.Size;
- if (index < _blocks.Count)
+ // Don't merge blocks from different allocations
+ if (index < _blocks.Count && endOffs % _regionSize != 0)
{
MemoryBlock next = _blocks[index];
- int endOffs = block.Offset + block.Size;
-
if (next.Offset == endOffs)
{
block = new MemoryBlock(block.Offset, block.Size + next.Size);
@@ -114,7 +134,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
- if (index > 0)
+ // Don't merge blocks from different allocations
+ if (index > 0 && block.Offset % _regionSize != 0)
{
MemoryBlock prev = _blocks[index - 1];
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
index a96e3554a..ee0710de2 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
@@ -11,76 +11,45 @@ using System.Threading;
namespace Ryujinx.Cpu.LightningJit.Cache
{
- static partial class JitCache
+ partial class JitCache : IDisposable
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
- // TODO: JIT Cache size should be application dependent, not global.
- private const int CacheSize = 1024 * (1024 * 1024); // Megabytes * Size of Megabytes (since its in bytes).
+ private const uint CacheSize = 256 * 1024 * 1024; // Megabytes * Size of Megabytes (since its in bytes).
- private static JitCacheInvalidation _jitCacheInvalidator;
+ private readonly JitCacheInvalidation _jitCacheInvalidator;
- private static CacheMemoryAllocator _cacheAllocator;
+ private readonly CacheMemoryAllocator _cacheAllocator;
- private static readonly List _cacheEntries = [];
+ private readonly List _cacheEntries = [];
- private static readonly Lock _lock = new();
- private static bool _initialized;
- private static readonly List _jitRegions = [];
- private static int _activeRegionIndex = 0;
-
- [SupportedOSPlatform("windows")]
- [LibraryImport("kernel32.dll", SetLastError = true)]
- public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
+ private readonly Lock _lock = new();
- [SupportedOSPlatform("macos")]
- [LibraryImport("libSystem.dylib", EntryPoint = "sys_icache_invalidate")]
- internal static partial void SysICacheInvalidate(nint start, nuint len);
+ private readonly List _jitRegions = [];
- [SupportedOSPlatform("linux")]
- [LibraryImport("libgcc_s.so.1", EntryPoint = "__clear_cache")]
- internal static partial void ClearCache(nint begin, nint end);
-
- public static void Initialize(IJitMemoryAllocator allocator)
+ public JitCache(IJitMemoryAllocator allocator)
{
- if (_initialized)
- {
- return;
- }
-
lock (_lock)
{
- if (_initialized)
- {
- return;
- }
-
- ReservedRegion firstRegion = new(allocator, CacheSize);
- _jitRegions.Add(firstRegion);
- _activeRegionIndex = 0;
-
+ _jitRegions.Add(new(allocator, CacheSize));
+
+ _cacheAllocator = new CacheMemoryAllocator((int)CacheSize);
+
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
-
- _cacheAllocator = new CacheMemoryAllocator(CacheSize);
-
- _initialized = true;
}
}
- public unsafe static nint Map(ReadOnlySpan code)
+ public nint Map(ReadOnlySpan code)
{
lock (_lock)
{
- Debug.Assert(_initialized);
-
int funcOffset = Allocate(code.Length);
- ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
- nint funcPtr = targetRegion.Pointer + funcOffset;
+ nint funcPtr = GetFunctionPtr(funcOffset);
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -94,9 +63,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
else
{
- ReprotectAsWritable(targetRegion, funcOffset, code.Length);
+ ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
- ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
+ ReprotectAsExecutable(funcOffset, code.Length);
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
@@ -107,21 +76,20 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
- public static void Unmap(nint pointer)
+ public void Unmap(nint pointer)
{
lock (_lock)
{
- Debug.Assert(_initialized);
-
- foreach (ReservedRegion region in _jitRegions)
+ for (int i = 0; i < _jitRegions.Count; i++)
{
+ ReservedRegion region = _jitRegions[i];
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
- pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
+ pointer.ToInt64() >= (region.Pointer + (nint)CacheSize).ToInt64())
{
continue;
}
- int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
+ int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64() + i * CacheSize);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
@@ -134,59 +102,63 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
- private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
+ private void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
- int regionStart = offset & ~_pageMask;
- int regionEnd = (endOffs + _pageMask) & ~_pageMask;
+ int regionStart = (offset % (int)CacheSize) & ~_pageMask;
+ int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
- region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
+ GetRegion(offset).Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
- private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
+ private void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
- int regionStart = offset & ~_pageMask;
- int regionEnd = (endOffs + _pageMask) & ~_pageMask;
+ int regionStart = (offset % (int)CacheSize) & ~_pageMask;
+ int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
- region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
+ GetRegion(offset).Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
- private static int Allocate(int codeSize)
+ private int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
+
+ int allocOffset = _cacheAllocator.Allocate(codeSize);
- for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
+ if (allocOffset >= 0)
{
- int allocOffset = _cacheAllocator.Allocate(codeSize);
-
- if (allocOffset >= 0)
- {
- _jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
- _activeRegionIndex = i;
- return allocOffset;
- }
+ GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
+ return allocOffset;
}
-
- int exhaustedRegion = _activeRegionIndex;
+
+ _cacheAllocator.AddNewBlocks(1);
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
+
+ Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache of size {(_jitRegions.Count * CacheSize).Bytes()} exhausted, creating new Cache Region ({((_jitRegions.Count + 1) * CacheSize).Bytes()} Total Allocation).");
+
_jitRegions.Add(newRegion);
- _activeRegionIndex = _jitRegions.Count - 1;
- int newRegionNumber = _activeRegionIndex;
-
- Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((long)(newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
-
- _cacheAllocator = new CacheMemoryAllocator(CacheSize);
-
- int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
- if (allocOffsetNew < 0)
+ allocOffset = _cacheAllocator.Allocate(codeSize);
+ if (allocOffset < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
- newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
- return allocOffsetNew;
+ GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
+ return allocOffset;
+ }
+
+ private nint GetFunctionPtr(int offset)
+ {
+ return GetRegion(offset).Pointer + (offset % (int)CacheSize);
+ }
+
+ private ReservedRegion GetRegion(int offset)
+ {
+ int index = offset / (int)CacheSize;
+
+ return _jitRegions[index];
}
private static int AlignCodeSize(int codeSize)
@@ -194,7 +166,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
}
- private static void Add(int offset, int size)
+ private void Add(int offset, int size)
{
CacheEntry entry = new(offset, size);
@@ -208,7 +180,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
_cacheEntries.Insert(index, entry);
}
- public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
+ public bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
@@ -231,5 +203,13 @@ namespace Ryujinx.Cpu.LightningJit.Cache
entryIndex = 0;
return false;
}
+
+ public void Dispose()
+ {
+ foreach (ReservedRegion jitRegion in _jitRegions)
+ {
+ jitRegion.Dispose();
+ }
+ }
}
}
diff --git a/src/Ryujinx.Cpu/LightningJit/Translator.cs b/src/Ryujinx.Cpu/LightningJit/Translator.cs
index 1ee6993d2..db77cf058 100644
--- a/src/Ryujinx.Cpu/LightningJit/Translator.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Translator.cs
@@ -19,6 +19,7 @@ namespace Ryujinx.Cpu.LightningJit
private static bool IsNoWxPlatform => false;
private readonly ConcurrentQueue> _oldFuncs;
+ private readonly JitCache _jitCache;
private readonly NoWxCache _noWxCache;
private bool _disposed;
@@ -39,12 +40,12 @@ namespace Ryujinx.Cpu.LightningJit
}
else
{
- JitCache.Initialize(new JitMemoryAllocator(forJit: true));
+ _jitCache = new(new JitMemoryAllocator(forJit: true));
}
Functions = new TranslatorCache();
FunctionTable = functionTable;
- Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
+ Stubs = new TranslatorStubs(_jitCache, FunctionTable, _noWxCache);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
@@ -100,7 +101,7 @@ namespace Ryujinx.Cpu.LightningJit
if (oldFunc != func)
{
- JitCache.Unmap(func.FuncPointer);
+ _jitCache.Unmap(func.FuncPointer);
func = oldFunc;
}
@@ -121,7 +122,7 @@ namespace Ryujinx.Cpu.LightningJit
private TranslatedFunction Translate(ulong address, ExecutionMode mode)
{
CompiledFunction func = Compile(address, mode);
- nint funcPointer = JitCache.Map(func.Code);
+ nint funcPointer = _jitCache.Map(func.Code);
return new TranslatedFunction(funcPointer, (ulong)func.GuestCodeLength);
}
@@ -164,14 +165,14 @@ namespace Ryujinx.Cpu.LightningJit
foreach (TranslatedFunction func in functions)
{
- JitCache.Unmap(func.FuncPointer);
+ _jitCache.Unmap(func.FuncPointer);
}
Functions.Clear();
while (_oldFuncs.TryDequeue(out KeyValuePair kv))
{
- JitCache.Unmap(kv.Value.FuncPointer);
+ _jitCache.Unmap(kv.Value.FuncPointer);
}
}
@@ -188,6 +189,7 @@ namespace Ryujinx.Cpu.LightningJit
else
{
ClearJitCache();
+ _jitCache.Dispose();
}
Stubs.Dispose();
diff --git a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
index d0eec7eff..fdd6929a7 100644
--- a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
+++ b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
@@ -19,6 +19,8 @@ namespace Ryujinx.Cpu.LightningJit
{
private delegate ulong GetFunctionAddressDelegate(nint framePointer, ulong address);
+ private readonly JitCache _jitCache;
+
private readonly Lazy _slowDispatchStub;
private bool _disposed;
@@ -76,13 +78,16 @@ namespace Ryujinx.Cpu.LightningJit
/// Initializes a new instance of the class with the specified
/// instance.
///
+ /// Jit cache to map functions in
/// Function table used to store pointers to the functions that the guest code will call
/// Cache used on platforms that enforce W^X, otherwise should be null
/// is null
- public TranslatorStubs(IAddressTable functionTable, NoWxCache noWxCache)
+ public TranslatorStubs(JitCache jitCache, IAddressTable functionTable, NoWxCache noWxCache)
{
ArgumentNullException.ThrowIfNull(functionTable);
+ _jitCache = jitCache;
+
_functionTable = functionTable;
_noWxCache = noWxCache;
_getFunctionAddressRef = NativeInterface.GetFunctionAddress;
@@ -113,12 +118,12 @@ namespace Ryujinx.Cpu.LightningJit
{
if (_dispatchStub.IsValueCreated)
{
- JitCache.Unmap(_dispatchStub.Value);
+ _jitCache.Unmap(_dispatchStub.Value);
}
if (_dispatchLoop.IsValueCreated)
{
- JitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
+ _jitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
}
}
@@ -363,7 +368,7 @@ namespace Ryujinx.Cpu.LightningJit
}
else
{
- return JitCache.Map(code);
+ return _jitCache.Map(code);
}
}
diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs b/src/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs
index 666b82792..235f29e89 100644
--- a/src/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs
+++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Decoder.cs
@@ -11,6 +11,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
public bool IsHardwareAccelerated => false;
private readonly MemoryAllocator _allocator = new();
+ private byte[] _lastSegmentMap = [];
+ private int _lastSegmentMapMiRows;
+ private int _lastSegmentMapMiCols;
public ISurface CreateSurface(int width, int height)
{
@@ -106,7 +109,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
cm.AllocTileWorkerData(_allocator, tileCols, tileRows, maxThreads);
cm.AllocContextBuffers(_allocator, output.Width, output.Height);
+
cm.InitContextBuffers();
+ LoadSegmentMap(ref cm, ref pictureInfo);
cm.SetupSegmentationDequant();
cm.SetupScaleFactors();
@@ -153,6 +158,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
}
}
+ SaveSegmentMap(ref cm);
cm.GetMvs(mvsOut);
cm.FreeTileWorkerData(_allocator);
@@ -161,6 +167,52 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
return true;
}
+ private void LoadSegmentMap(ref Vp9Common cm, ref Vp9PictureInfo pictureInfo)
+ {
+ int mapLength = cm.MiRows * cm.MiCols;
+ if (mapLength == 0 || cm.LastFrameSegMap.IsNull)
+ {
+ return;
+ }
+
+ bool reset =
+ pictureInfo.IsKeyFrame ||
+ pictureInfo.IntraOnly ||
+ _lastSegmentMapMiRows != cm.MiRows ||
+ _lastSegmentMapMiCols != cm.MiCols;
+
+ if (_lastSegmentMap.Length != mapLength)
+ {
+ _lastSegmentMap = new byte[mapLength];
+ }
+ else if (reset)
+ {
+ Array.Clear(_lastSegmentMap);
+ }
+
+ _lastSegmentMapMiRows = cm.MiRows;
+ _lastSegmentMapMiCols = cm.MiCols;
+ _lastSegmentMap.CopyTo(cm.LastFrameSegMap.AsSpan());
+ }
+
+ private void SaveSegmentMap(ref Vp9Common cm)
+ {
+ int mapLength = cm.MiRows * cm.MiCols;
+ if (!cm.Seg.Enabled || mapLength == 0 || cm.CurrentFrameSegMap.IsNull)
+ {
+ return;
+ }
+
+ if (_lastSegmentMap.Length != mapLength)
+ {
+ _lastSegmentMap = new byte[mapLength];
+ }
+
+ cm.CurrentFrameSegMap.AsSpan()[..mapLength].CopyTo(_lastSegmentMap);
+ _lastSegmentMapMiRows = cm.MiRows;
+ _lastSegmentMapMiCols = cm.MiCols;
+ }
+
public void Dispose()
{
_allocator.Dispose();
diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs b/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs
index b4eda171f..77583eaa7 100644
--- a/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs
+++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Types/Vp9Common.cs
@@ -1088,6 +1088,12 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types
public void GetMvs(Span mvs)
{
+ if (FrameIsIntraOnly())
+ {
+ mvs.Clear();
+ return;
+ }
+
if (mvs.Length > CurFrameMvs.Length)
{
throw new ArgumentException(
diff --git a/src/Ryujinx.Graphics.Nvdec/Types/Vp9/PictureInfo.cs b/src/Ryujinx.Graphics.Nvdec/Types/Vp9/PictureInfo.cs
index 9da64bfab..a6fb3689b 100644
--- a/src/Ryujinx.Graphics.Nvdec/Types/Vp9/PictureInfo.cs
+++ b/src/Ryujinx.Graphics.Nvdec/Types/Vp9/PictureInfo.cs
@@ -53,6 +53,7 @@ namespace Ryujinx.Graphics.Nvdec.Types.Vp9
IsKeyFrame = Flags.HasFlag(FrameFlags.IsKeyFrame),
IntraOnly = Flags.HasFlag(FrameFlags.IntraOnly),
UsePrevInFindMvRefs =
+ !Flags.HasFlag(FrameFlags.IsKeyFrame) &&
!Flags.HasFlag(FrameFlags.ErrorResilientMode) &&
!Flags.HasFlag(FrameFlags.FrameSizeChanged) &&
!Flags.HasFlag(FrameFlags.IntraOnly) &&
diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
index 5acee3bb7..21a4222a2 100644
--- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
@@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private readonly bool[] _supportedPlayers;
private VibrationValue _neutralVibrationValue = new()
{
- AmplitudeLow = 0f,
+ AmplitudeLow = 0.01f,
FrequencyLow = 160f,
AmplitudeHigh = 0f,
FrequencyHigh = 320f,
diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs
index 8cca7cc6e..8af014be8 100644
--- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs
@@ -11,7 +11,11 @@ namespace Ryujinx.HLE.HOS.Services.Notification
private readonly KEvent _getNotificationSendingNotifierEvent;
private int _getNotificationSendingNotifierEventHandle;
- public INotificationSystemEventAccessor(ServiceCtx context) { }
+
+ public INotificationSystemEventAccessor(ServiceCtx context)
+ {
+ _getNotificationSendingNotifierEvent = new KEvent(context.Device.System.KernelContext);
+ }
[CommandCmif(0)] // 9.0.0+
// GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor
diff --git a/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs b/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
index 45c4ce7e1..5f78028e0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs
@@ -103,8 +103,12 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
loadedCount++;
}
+ // The first return value is a boolean "fonts_are_loaded" flag (u8), not a count.
+ // Some titles (e.g. newer SDK builds) validate it strictly against 1, so writing
+ // the font count here (e.g. 6) makes them reject the result and spin re-opening pl:u.
+ context.ResponseData.Write(1);
context.ResponseData.Write(loadedCount);
- context.ResponseData.Write((int)SharedFontType.Count);
+ // context.ResponseData.Write((int)SharedFontType.Count);
return ResultCode.Success;
}
diff --git a/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs b/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs
index a3b4e0f83..9f8f9cafb 100644
--- a/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs
+++ b/src/Ryujinx.Horizon/MmNv/Ipc/Request.cs
@@ -16,10 +16,9 @@ namespace Ryujinx.Horizon.MmNv.Ipc
public Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent)
{
bool isAutoClearEvent = autoClearEvent != 0;
+ uint requestId = Register(module, fgmPriority, isAutoClearEvent);
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
-
- Register(module, fgmPriority, isAutoClearEvent);
+ Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
return Result.Success;
}
@@ -27,41 +26,48 @@ namespace Ryujinx.Horizon.MmNv.Ipc
[CmifCommand(1)]
public Result FinalizeOld(Module module)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
-
lock (_sessionList)
{
- _sessionList.Remove(GetSessionByModule(module));
+ Session session = GetSessionByModule(module);
+
+ if (session != null)
+ {
+ _sessionList.Remove(session);
+ }
}
+ Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized module={module}");
+
return Result.Success;
}
[CmifCommand(2)]
- public Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax)
+ public Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, clockRateMin, clockRateMax });
+ uint actualClockRate;
lock (_sessionList)
{
- GetSessionByModule(module)?.SetAndWait(clockRateMin, clockRateMax);
+ Session session = GetSessionByModule(module) ?? RegisterSession(module, false);
+ ApplyClockRequest(session, clockRateMin, timeout);
+ actualClockRate = GetEffectiveClockRate(module);
}
+ Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait module={module} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
+
return Result.Success;
}
[CmifCommand(3)]
public Result GetOld(out uint clockRateActual, Module module)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
-
lock (_sessionList)
{
- Session session = GetSessionByModule(module);
-
- clockRateActual = session == null ? 0 : session.ClockRateMin;
+ clockRateActual = GetEffectiveClockRate(module);
}
+ Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get module={module} actual={clockRateActual}");
+
return Result.Success;
}
@@ -70,51 +76,76 @@ namespace Ryujinx.Horizon.MmNv.Ipc
{
bool isAutoClearEvent = autoClearEvent != 0;
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
-
requestId = Register(module, fgmPriority, isAutoClearEvent);
+ Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
+
return Result.Success;
}
[CmifCommand(5)]
public Result Finalize(uint requestId)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
-
lock (_sessionList)
{
- _sessionList.Remove(GetSessionById(requestId));
+ Session session = GetSessionById(requestId);
+
+ if (session != null)
+ {
+ _sessionList.Remove(session);
+ }
}
+ Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized requestId={requestId}");
+
return Result.Success;
}
[CmifCommand(6)]
- public Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax)
+ public Result SetAndWait(uint requestId, uint clockRateMin, uint timeout)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId, clockRateMin, clockRateMax });
+ uint actualClockRate = 0;
+ Module? module = null;
lock (_sessionList)
{
- GetSessionById(requestId)?.SetAndWait(clockRateMin, clockRateMax);
+ Session session = GetSessionById(requestId);
+
+ if (session != null)
+ {
+ module = session.Module;
+ ApplyClockRequest(session, clockRateMin, timeout);
+ actualClockRate = GetEffectiveClockRate(session.Module);
+ }
}
+ Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait requestId={requestId} module={module?.ToString() ?? ""} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
+
return Result.Success;
}
[CmifCommand(7)]
public Result Get(out uint clockRateActual, uint requestId)
{
- Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
+ Module? module = null;
lock (_sessionList)
{
Session session = GetSessionById(requestId);
- clockRateActual = session == null ? 0 : session.ClockRateMin;
+ if (session == null)
+ {
+ clockRateActual = 0;
+ }
+ else
+ {
+ module = session.Module;
+ clockRateActual = GetEffectiveClockRate(session.Module);
+ }
}
+ Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get requestId={requestId} module={module?.ToString() ?? ""} actual={clockRateActual}");
+
return Result.Success;
}
@@ -149,12 +180,51 @@ namespace Ryujinx.Horizon.MmNv.Ipc
lock (_sessionList)
{
// Nintendo ignores the fgm priority as the other services were deprecated.
- Session session = new(_uniqueId++, module, isAutoClearEvent);
-
- _sessionList.Add(session);
+ Session session = RegisterSession(module, isAutoClearEvent);
return session.Id;
}
}
+
+ private Session RegisterSession(Module module, bool isAutoClearEvent)
+ {
+ Session session = new(_uniqueId++, module, isAutoClearEvent);
+
+ _sessionList.Add(session);
+
+ return session;
+ }
+
+ private void ApplyClockRequest(Session session, uint requestedClockRate, uint timeout)
+ {
+ if (IsClockRateSentinel(requestedClockRate))
+ {
+ session.ClearRequest(timeout);
+ }
+ else
+ {
+ session.SetAndWait(requestedClockRate, timeout);
+ }
+ }
+
+ private uint GetEffectiveClockRate(Module module)
+ {
+ uint clockRate = 0;
+
+ foreach (Session session in _sessionList)
+ {
+ if (session.Module == module && session.HasActiveRequest && session.RequestedClockRate > clockRate)
+ {
+ clockRate = session.RequestedClockRate;
+ }
+ }
+
+ return clockRate;
+ }
+
+ private static bool IsClockRateSentinel(uint clockRate)
+ {
+ return clockRate == 0 || unchecked((int)clockRate) < 0;
+ }
}
}
diff --git a/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs b/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs
index 1c2ce6552..dadc37fec 100644
--- a/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs
+++ b/src/Ryujinx.Horizon/Sdk/MmNv/IRequest.cs
@@ -7,11 +7,11 @@ namespace Ryujinx.Horizon.Sdk.MmNv
{
Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent);
Result FinalizeOld(Module module);
- Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax);
+ Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout);
Result GetOld(out uint clockRateActual, Module module);
Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent);
Result Finalize(uint requestId);
- Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax);
+ Result SetAndWait(uint requestId, uint clockRateMin, uint timeout);
Result Get(out uint clockRateActual, uint requestId);
}
}
diff --git a/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs b/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs
index b67a9d226..1858eed12 100644
--- a/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs
+++ b/src/Ryujinx.Horizon/Sdk/MmNv/Session.cs
@@ -5,22 +5,32 @@ namespace Ryujinx.Horizon.Sdk.MmNv
public Module Module { get; }
public uint Id { get; }
public bool IsAutoClearEvent { get; }
- public uint ClockRateMin { get; private set; }
- public int ClockRateMax { get; private set; }
+ public uint RequestedClockRate { get; private set; }
+ public uint LastTimeout { get; private set; }
+ public bool HasActiveRequest { get; private set; }
public Session(uint id, Module module, bool isAutoClearEvent)
{
Module = module;
Id = id;
IsAutoClearEvent = isAutoClearEvent;
- ClockRateMin = 0;
- ClockRateMax = -1;
+ RequestedClockRate = 0;
+ LastTimeout = uint.MaxValue;
+ HasActiveRequest = false;
}
- public void SetAndWait(uint clockRateMin, int clockRateMax)
+ public void SetAndWait(uint clockRate, uint timeout)
{
- ClockRateMin = clockRateMin;
- ClockRateMax = clockRateMax;
+ RequestedClockRate = clockRate;
+ LastTimeout = timeout;
+ HasActiveRequest = true;
+ }
+
+ public void ClearRequest(uint timeout)
+ {
+ RequestedClockRate = 0;
+ LastTimeout = timeout;
+ HasActiveRequest = false;
}
}
}
diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
index 408b5213b..0356df172 100644
--- a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
+++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
@@ -13,84 +13,98 @@ namespace Ryujinx.Input.SDL3
{
private readonly SDL_hid_device* _hidHandle;
+ private byte[] _buffer;
+ private static ushort _vendor;
+ private static ushort _product;
+
private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle)
{
_hidHandle = hidHandle;
+ InitializeDevice();
}
public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
{
- ushort vendor = SDL_GetGamepadVendor(gamepadHandle);
- if (vendor != 0x057e)
+ _vendor = SDL_GetGamepadVendor(gamepadHandle);
+ if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor))
{
return null;
}
- ushort product = SDL_GetGamepadProduct(gamepadHandle);
- if (!Enum.IsDefined(typeof(HDRumbleSupported), product))
+ _product = SDL_GetGamepadProduct(gamepadHandle);
+ if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product))
{
return null;
}
- return new NpadHdRumble(SDL_hid_open(vendor, product, 0));
+ int serialNumber = 0;
+ string serial = SDL_GetGamepadSerial(gamepadHandle);
+ if (serial is not null)
+ {
+ int.TryParse(serial, out serialNumber);
+ }
+
+ return new NpadHdRumble(SDL_hid_open(_vendor, _product, serialNumber));
}
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
- private bool WriteHdRumble(
- int encLeftLowFreq, int encLeftLowAmp,
- int encLeftHighFreq, int encLeftHighAmp,
- int encRightLowFreq, int encRightLowAmp,
- int encRightHighFreq, int encRightHighAmp)
+ private bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right)
{
- byte[] buf = new byte[10];
-
- buf[0] = 0x10;
- buf[1] = (byte)((++_globalCount) & 0xF);
-
- buf[2] = (byte)(encLeftHighFreq & 0xFF);
- buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF));
- buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF));
- buf[5] = (byte)(encLeftLowAmp & 0xFF);
-
- buf[6] = (byte)(encRightHighFreq & 0xFF);
- buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF));
- buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF));
- buf[9] = (byte)(encRightLowAmp & 0xFF);
-
+ int leftLowAmp = EncodeLowAmp(left.AmplitudeLow);
+ int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8);
+ int leftHighFreq = EncodeHighFreq(left.FrequencyHigh);
+ int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8);
+
+ int rightLowAmp = EncodeLowAmp(right.AmplitudeLow);
+ int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8);
+ int rightHighFreq = EncodeHighFreq(right.FrequencyHigh);
+ int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8);
+
+ _buffer[0] = 0x10;
+ _buffer[1] = (byte)((_globalCount++) & 0xF);
+
+ // Left LRA
+ _buffer[2] = (byte)(leftLowFreq & 0xFF);
+ _buffer[3] = (byte)(leftHighAmp & 0xFF);
+ _buffer[4] = (byte)(leftHighFreq & 0xFF);
+ _buffer[5] = (byte)(leftLowAmp & 0xFF);
+
+ // Right LRA
+ _buffer[6] = (byte)(rightLowFreq & 0xFF);
+ _buffer[7] = (byte)(rightHighAmp & 0xFF);
+ _buffer[8] = (byte)(rightHighFreq & 0xFF);
+ _buffer[9] = (byte)(rightLowAmp & 0xFF);
+
if (_globalCount > 0xF)
{
_globalCount = 0x0;
}
-
- fixed (byte* ptr = buf)
+
+ fixed (byte* ptr = _buffer)
{
- if (SendHDRumble(ptr, (nuint)buf.Length) >= 0)
+ if (SendHdRumble(ptr, (nuint)_buffer.Length) >= 0)
{
return true;
}
- if (!String.IsNullOrEmpty(SDL_GetError()))
- {
- Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
- SDL_ClearError();
- }
- return false;
+ Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
+ SDL_ClearError();
}
+
+ return false;
}
private static int EncodeLowFreq(float lowFreq)
{
- float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f);
- return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40);
+ return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f);
}
private static int EncodeHighFreq(float highFreq)
{
- float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f);
- return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4);
+ return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f);
}
private static int EncodeLowAmp(float rawAmp)
@@ -98,23 +112,20 @@ namespace Ryujinx.Input.SDL3
double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
- {
encodedAmp = 1;
- }
+
else if (rawAmp is >= 0.012f and < 0.112f)
- {
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
- }
+
else if (rawAmp is >= 0.112f and < 0.225f)
- {
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
- }
+
else if (rawAmp is >= 0.225f and <= 1f)
- {
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
- }
-
- return (int)Math.Floor(encodedAmp / 2.0) + 64;
+
+ encodedAmp = Math.Round((encodedAmp / 2.0) + 64.0);
+ encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
+ return (int)Math.Round(encodedAmp);
}
private static int EncodeHighAmp(float rawAmp)
@@ -122,82 +133,156 @@ namespace Ryujinx.Input.SDL3
double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
- {
encodedAmp = 1;
- }
+
else if (rawAmp is >= 0.012f and < 0.112f)
- {
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
- }
+
else if (rawAmp is >= 0.112f and < 0.225f)
- {
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
- }
+
else if (rawAmp is >= 0.225f and <= 1f)
- {
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
- }
-
- return (int) Math.Round(encodedAmp * 2);
+
+ encodedAmp = Math.Round(encodedAmp / 2.0);
+ encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
+ return (int)encodedAmp;
}
public bool HdRumble(VibrationValue left, VibrationValue right)
{
- return WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
- EncodeLowAmp(left.AmplitudeLow),
- EncodeHighFreq(left.FrequencyHigh),
- EncodeHighAmp(left.AmplitudeHigh),
- EncodeLowFreq(right.FrequencyLow),
- EncodeLowAmp(right.AmplitudeLow),
- EncodeHighFreq(right.FrequencyHigh),
- EncodeHighAmp(right.AmplitudeHigh));
+ if(_product is (ushort) HDRumbleSupportedProduct.ProController
+ or (ushort) HDRumbleSupportedProduct.JoyconLeft
+ or (ushort) HDRumbleSupportedProduct.JoyconRight
+ or (ushort) HDRumbleSupportedProduct.JoyconPair
+ or (ushort) HDRumbleSupportedProduct.JoyconGrip)
+ {
+ return WriteNintendoHdRumble(left, right);
+ }
+
+ return false;
}
- private int SendHDRumble(byte* data, nuint length)
+ private int SendHdRumble(byte* data, nuint length)
{
int result = 0;
ulong currentTicks = SDL_GetTicks();
// Ditch rumble if we haven't hit the poll-rate yet.
- // TODO: figure out a better way to do this
- // While the polling check makes the rumble accurate, it also causes it to miss signals.
- if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX
+ if ((currentTicks - _lastWriteTicks) <= GetPollRate())
{
return result;
}
- SDL_LockJoysticks();
+ result = SDL_hid_write(_hidHandle, data, length);
+ if (result >= 0)
{
- // Fun fact: Mario Kart 8 Deluxe sends rumble packets
- // where the amplitude is zero, but the frequency isn't.
- result = SDL_hid_write(_hidHandle, data, length);
- if (result >= 0)
- {
- _lastWriteTicks = currentTicks;
- }
+ _lastWriteTicks = currentTicks;
}
- SDL_UnlockJoysticks();
return result;
}
+ private void InitializeDevice()
+ {
+ if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
+ {
+ _buffer = new byte[10];
+ byte[] init = new byte[64];
+
+ // Pro Controller and Charge Grip
+ if (_product
+ is (ushort)HDRumbleSupportedProduct.ProController
+ or (ushort)HDRumbleSupportedProduct.JoyconGrip)
+ {
+ SDL_LockJoysticks();
+ fixed (byte* ptr = init)
+ {
+ init[0] = 0x80;
+ init[1] = 0x05; // Allow bluetooth timeout TODO: use 0x04 to force USB only (toggle?)
+ SDL_hid_write(_hidHandle, ptr, 64);
+ }
+ SDL_UnlockJoysticks();
+
+ return;
+ }
+
+ // Joycons
+ if (_product
+ is (ushort)HDRumbleSupportedProduct.JoyconLeft
+ or (ushort)HDRumbleSupportedProduct.JoyconRight
+ or (ushort)HDRumbleSupportedProduct.JoyconPair)
+ {
+
+ SDL_LockJoysticks();
+ fixed (byte* ptr = init)
+ {
+ // we could write data to the controller here (see above)
+ }
+ SDL_UnlockJoysticks();
+
+ return;
+ }
+ }
+ }
+
+ private ulong GetPollRate()
+ {
+ ulong pollRate = 0;
+ if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
+ {
+ // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX
+ pollRate = (ulong) 16.67;
+ if (_product is (ushort)HDRumbleSupportedProduct.ProController
+ && SDL_hid_get_device_info(_hidHandle)->bus_type == SDL_hid_bus_type.SDL_HID_API_BUS_USB)
+ {
+ pollRate = (ulong) 8.33;
+ }
+ }
+
+ return pollRate;
+ }
+
public void Dispose()
{
+ GC.SuppressFinalize(this);
SDL_hid_close(_hidHandle);
}
}
- public enum HDRumbleSupported : ushort
+ public enum HDRumbleSupportedVendor : ushort
{
- JoyConLeft = 0x2006,
- JoyConRight = 0x2007,
+ Nintendo = 0x057e,
+ Valve = 0x28de,
+ Sony = 0x054c
+ }
+
+ public enum HDRumbleSupportedProduct : ushort
+ {
+ // TODO: Currently, HD Rumble only supports the Pro Controller and JoyCons.
+ // We need to initialize and report to each device differently.
+
+ // Nintendo Switch: 0x057e
+ JoyconLeft = 0x2006,
+ JoyconRight = 0x2007,
JoyconPair = 0x2008,
ProController = 0x2009,
JoyconGrip = 0x200e,
+
+ // Nintendo Switch 2: 0x057e
Joycon2Right = 0x2066,
Joycon2Left = 0x2067,
Joycon2Pair = 0x2068,
Switch2ProController = 0x2069,
- GamecubeController = 0x2073
+ GamecubeController = 0x2073,
+
+ // Valve Steam Family: 0x28de
+ // https://github.com/libsdl-org/SDL/issues/9148
+ SteamDeck = 0x11ff,
+ SteamDeckVirtualDevice = 0x1205,
+ SteamController = 0x1106,
+
+ // PlayStation Dualsense: 0x054c
+ Dualsense = 0x0ce6
}
}
diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
index 47ef0b59f..36b29ee19 100644
--- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
+++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
@@ -275,7 +275,7 @@ namespace Ryujinx.Input.SDL3
{
_configuration = (StandardControllerInputConfig)configuration;
- if ((Features & GamepadFeaturesFlag.Led) != 0 && _configuration.Led.EnableLed)
+ if ((Features & GamepadFeaturesFlag.Led) != 0 && _configuration.Led?.EnableLed == true)
{
if (_configuration.Led.TurnOffLed)
(this as IGamepad).ClearLed();
diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs
index 75e04ae40..f0c270277 100644
--- a/src/Ryujinx.Input/HLE/NpadController.cs
+++ b/src/Ryujinx.Input/HLE/NpadController.cs
@@ -1,14 +1,20 @@
using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
using ConfigControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
@@ -16,6 +22,9 @@ namespace Ryujinx.Input.HLE
{
public class NpadController : IDisposable
{
+ private const string KeyboardString = "keyboard";
+ private const string ControllerString = "controller";
+
private class HLEButtonMappingEntry
{
public readonly GamepadButtonInputId DriverInputId;
@@ -211,14 +220,40 @@ namespace Ryujinx.Input.HLE
private MotionInput _rightMotionInput;
private IGamepad _gamepad;
+ private IGamepad _keyboardGamepad;
+ private IGamepad _controllerGamepad;
+ private readonly List _assignedControllerGamepads = [];
+ private readonly List _assignedControllerConfigs = [];
private InputConfig _config;
+ private InputConfig _activeConfig;
+ private StandardKeyboardInputConfig _keyboardConfig;
+ private StandardControllerInputConfig _controllerConfig;
+ private GamepadStateSnapshot _previousKeyboardState;
+ private readonly List _previousControllerStates = [];
+ private DynamicInputSource _activeInputSource;
+ private PlayerInputAssignment _playerInputAssignment;
+ private bool _singleUsesKeyboardDriver;
+ private IGamepadDriver _keyboardDriver;
+ private IGamepadDriver _controllerDriver;
+ private int _activeControllerIndex = -1;
public IGamepadDriver GamepadDriver { get; private set; }
public GamepadStateSnapshot State { get; private set; }
+ public InputConfig ActiveConfig => _activeConfig;
public string Id { get; private set; }
+ public bool IsAvailable => _gamepad != null || _keyboardGamepad != null || _assignedControllerGamepads.Count > 0;
+
private readonly CemuHookClient _cemuHookClient;
+ private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ private enum DynamicInputSource
+ {
+ None,
+ Keyboard,
+ Controller,
+ }
public NpadController(CemuHookClient cemuHookClient)
{
@@ -227,31 +262,114 @@ namespace Ryujinx.Input.HLE
_cemuHookClient = cemuHookClient;
}
- public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config)
+ public bool MatchesDriverConfiguration(InputConfig config, PlayerInputAssignment playerInputAssignment)
{
- GamepadDriver = gamepadDriver;
+ if (_config?.EnableDynamicGamepadSwap != config.EnableDynamicGamepadSwap)
+ {
+ return false;
+ }
- _gamepad?.Dispose();
+ if (playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ if (_playerInputAssignment == null || _playerInputAssignment.EnableDynamicInputSwap != playerInputAssignment.EnableDynamicInputSwap)
+ {
+ return false;
+ }
- Id = config.Id;
- _gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
- ? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
- : GamepadDriver.GetGamepad(Id);
+ return PlayerInputAssignmentHelper.AreEquivalent(_playerInputAssignment, playerInputAssignment);
+ }
+
+ return _singleUsesKeyboardDriver == (config is StandardKeyboardInputConfig) &&
+ Id == config.Id;
+ }
+
+ public bool UpdateDriverConfiguration(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, InputConfig config, PlayerInputAssignment playerInputAssignment)
+ {
+ _keyboardDriver = keyboardDriver;
+ _controllerDriver = gamepadDriver;
+ _playerInputAssignment = playerInputAssignment;
+
+ DisposeOpenedGamepads();
+
+ _gamepad = null;
+ _keyboardGamepad = null;
+ _controllerGamepad = null;
+ _assignedControllerGamepads.Clear();
+ _assignedControllerConfigs.Clear();
+ _previousKeyboardState = default;
+ _previousControllerStates.Clear();
+ _activeInputSource = DynamicInputSource.None;
+ _activeControllerIndex = -1;
+
+ if (playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ ConfigureDynamicGamepads(keyboardDriver, gamepadDriver, config);
+ }
+ else
+ {
+ _singleUsesKeyboardDriver = config is StandardKeyboardInputConfig;
+ GamepadDriver = _singleUsesKeyboardDriver ? keyboardDriver : gamepadDriver;
+ Id = config.Id;
+ _gamepad = OpenSingleGamepad(GamepadDriver, config.Id, _singleUsesKeyboardDriver);
+ }
UpdateUserConfiguration(config);
- return _gamepad != null;
+ return IsAvailable;
}
public void UpdateUserConfiguration(InputConfig config)
{
+ InputConfig oldConfig = _config;
+
+ if (_playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ StandardControllerInputConfig oldControllerConfig = _controllerConfig;
+
+ _config = config;
+ UpdateDynamicConfigurations(config);
+
+ if (_controllerConfig?.Motion == null)
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ }
+ else if (NeedsMotionInputUpdate(oldControllerConfig, _controllerConfig))
+ {
+ UpdateMotionInput(_controllerConfig.Motion);
+ }
+
+ if (_keyboardConfig != null)
+ {
+ _keyboardGamepad?.SetConfiguration(_keyboardConfig);
+ }
+
+ for (int i = 0; i < _assignedControllerGamepads.Count; i++)
+ {
+ StandardControllerInputConfig assignedControllerConfig = i < _assignedControllerConfigs.Count
+ ? _assignedControllerConfigs[i]
+ : _controllerConfig;
+
+ if (assignedControllerConfig != null)
+ {
+ _assignedControllerGamepads[i].SetConfiguration(assignedControllerConfig);
+ }
+ }
+
+ UpdateActiveGamepad();
+ return;
+ }
+
+ _config = config;
+
if (config is StandardControllerInputConfig controllerConfig)
{
- bool needsMotionInputUpdate = _config is not StandardControllerInputConfig oldControllerConfig ||
- ((oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) &&
- (oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend));
-
- if (needsMotionInputUpdate)
+ if (controllerConfig.Motion == null)
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ }
+ else if (NeedsMotionInputUpdate(oldConfig as StandardControllerInputConfig, controllerConfig))
{
UpdateMotionInput(controllerConfig.Motion);
}
@@ -260,15 +378,23 @@ namespace Ryujinx.Input.HLE
{
// Non-controller doesn't have motions.
_leftMotionInput = null;
+ _rightMotionInput = null;
}
- _config = config;
+ _activeConfig = config;
_gamepad?.SetConfiguration(config);
}
private void UpdateMotionInput(MotionConfigController motionConfig)
{
+ if (motionConfig == null)
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ return;
+ }
+
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
{
_leftMotionInput = new MotionInput();
@@ -281,72 +407,50 @@ namespace Ryujinx.Input.HLE
}
}
+ private bool NeedsMotionInputUpdate(StandardControllerInputConfig oldConfig, StandardControllerInputConfig newConfig)
+ {
+ if (newConfig?.Motion == null)
+ {
+ return false;
+ }
+
+ bool motionWasDisabled = oldConfig?.Motion == null;
+ bool leftMotionMissing = _leftMotionInput == null;
+ bool isJoyconPairNeedingRightMotion = newConfig.ControllerType == ConfigControllerType.JoyconPair && _rightMotionInput == null;
+ bool motionEnabledChanged = !motionWasDisabled && oldConfig?.Motion?.EnableMotion != newConfig.Motion.EnableMotion;
+ bool motionBackendChanged = !motionWasDisabled && oldConfig?.Motion?.MotionBackend != newConfig.Motion.MotionBackend;
+
+ return motionWasDisabled ||
+ leftMotionMissing ||
+ isJoyconPairNeedingRightMotion ||
+ motionEnabledChanged ||
+ motionBackendChanged;
+ }
+
public void Update()
{
+ if (_playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ UpdateDynamic();
+ return;
+ }
+
// _gamepad may be altered by other threads
IGamepad gamepad = _gamepad;
if (gamepad != null && GamepadDriver != null)
{
State = gamepad.GetMappedStateSnapshot();
+ _activeConfig = _config;
- if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
+ if (_activeConfig is StandardControllerInputConfig controllerConfig && controllerConfig.Motion?.EnableMotion == true)
{
- if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
- {
- if ((gamepad.Features & GamepadFeaturesFlag.Motion) != 0)
- {
- Vector3 accelerometer = gamepad.GetMotionData(MotionInputId.Accelerometer);
- Vector3 gyroscope = gamepad.GetMotionData(MotionInputId.Gyroscope);
-
- accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
- gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y);
-
- _leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
-
- if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
- {
- if (gamepad.Id == "JoyConPair")
- {
- Vector3 rightAccelerometer = gamepad.GetMotionData(MotionInputId.SecondAccelerometer);
- Vector3 rightGyroscope = gamepad.GetMotionData(MotionInputId.SecondGyroscope);
-
- rightAccelerometer = new Vector3(rightAccelerometer.X, -rightAccelerometer.Z, rightAccelerometer.Y);
- rightGyroscope = new Vector3(rightGyroscope.X, -rightGyroscope.Z, rightGyroscope.Y);
-
- _rightMotionInput.Update(rightAccelerometer, rightGyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
- }
- else
- {
- _rightMotionInput = _leftMotionInput;
- }
- }
- }
- }
- else if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook && controllerConfig.Motion is CemuHookMotionConfigController cemuControllerConfig)
- {
- int clientId = (int)controllerConfig.PlayerIndex;
-
- // First of all ensure we are registered
- _cemuHookClient.RegisterClient(clientId, cemuControllerConfig.DsuServerHost, cemuControllerConfig.DsuServerPort);
-
- // Then request and retrieve the data
- _cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot);
- _cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _leftMotionInput);
-
- if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
- {
- if (!cemuControllerConfig.MirrorInput)
- {
- _cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot);
- _cemuHookClient.TryGetData(clientId, cemuControllerConfig.AltSlot, out _rightMotionInput);
- }
- else
- {
- _rightMotionInput = _leftMotionInput;
- }
- }
- }
+ UpdateControllerMotion(gamepad, controllerConfig);
+ }
+ else
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
}
}
else
@@ -371,7 +475,7 @@ namespace Ryujinx.Input.HLE
}
}
- if (_gamepad is IKeyboard)
+ if (_activeConfig is StandardKeyboardInputConfig)
{
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
@@ -388,7 +492,7 @@ namespace Ryujinx.Input.HLE
Dy = ClampAxis(rightAxisY),
};
}
- else if (_config is StandardControllerInputConfig controllerConfig)
+ else if (_activeConfig is StandardControllerInputConfig controllerConfig)
{
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
@@ -509,9 +613,12 @@ namespace Ryujinx.Input.HLE
return value;
}
- public static KeyboardInput GetHLEKeyboardInput(IGamepadDriver KeyboardDriver)
+ public static KeyboardInput GetHLEKeyboardInput(IGamepadDriver keyboardDriver)
{
- IKeyboard keyboard = KeyboardDriver.GetGamepad("0") as IKeyboard;
+ if (keyboardDriver.GetGamepad("0") is not IKeyboard keyboard)
+ {
+ return default;
+ }
KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot();
@@ -543,7 +650,7 @@ namespace Ryujinx.Input.HLE
{
if (disposing)
{
- _gamepad?.Dispose();
+ DisposeOpenedGamepads();
}
}
@@ -557,38 +664,567 @@ namespace Ryujinx.Input.HLE
{
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
{
- if (_config is not StandardControllerInputConfig controllerConfig ||
- !controllerConfig.Rumble.EnableRumble)
+ if (_controllerConfig is StandardControllerInputConfig dynamicControllerConfig &&
+ _playerInputAssignment?.EnableDynamicInputSwap == true &&
+ dynamicControllerConfig.Rumble?.EnableRumble == true)
{
- return;
+ ApplyRumble(_controllerGamepad ?? _assignedControllerGamepads.FirstOrDefault(), dynamicControllerConfig, dualVibrationValue);
+ }
+ else if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble?.EnableRumble == true)
+ {
+ ApplyRumble(_gamepad, controllerConfig, dualVibrationValue);
+ }
+ }
+ }
+ public bool HasAssignedControllerId(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ return false;
+ }
+
+ if (_playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ return _assignedControllerGamepads.Any(gamepad => gamepad?.Id == id);
+ }
+
+ return Id == id;
+ }
+
+ private void ApplyRumble(IGamepad gamepad, StandardControllerInputConfig controllerConfig, (VibrationValue, VibrationValue) dualVibrationValue)
+ {
+ if (gamepad == null)
+ {
+ return;
+ }
+
+ VibrationValue leftVibrationValue = dualVibrationValue.Item1;
+ VibrationValue rightVibrationValue = dualVibrationValue.Item2;
+
+ float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
+ float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
+
+ leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
+ leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
+ rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
+ rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
+
+ if (!controllerConfig.Rumble.UseHDRumble || !gamepad.HDRumble(leftVibrationValue, rightVibrationValue))
+ {
+ gamepad.Rumble(low, high, 0xFFFFFFFF);
+ }
+
+ Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
+ // Value=value/multiplier * multiplier (result)
+ $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
+ $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " +
+ $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
+ $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " +
+ $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " +
+ $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
+ $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " +
+ $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})");
+ }
+
+ private void ConfigureDynamicGamepads(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, InputConfig config)
+ {
+ AssignedInputDevice assignedKeyboard = _playerInputAssignment?.Devices.FirstOrDefault(device => device.Type == AssignedInputDeviceType.Keyboard);
+
+ if (!string.IsNullOrEmpty(assignedKeyboard?.Id))
+ {
+ _keyboardGamepad = OpenSingleGamepad(keyboardDriver, assignedKeyboard.Id, true);
+ }
+
+ foreach (AssignedInputDevice assignedController in ResolveDynamicControllerAssignments(gamepadDriver, config))
+ {
+ IGamepad controllerGamepad = OpenSingleGamepad(gamepadDriver, assignedController.Id, false);
+
+ if (controllerGamepad != null)
+ {
+ _assignedControllerGamepads.Add(controllerGamepad);
+ _assignedControllerConfigs.Add(null);
+ _previousControllerStates.Add(default);
+ }
+ }
+
+ _controllerGamepad = _assignedControllerGamepads.FirstOrDefault();
+ GamepadDriver = null;
+ Id = _assignedControllerGamepads.FirstOrDefault()?.Id ?? config.Id;
+ }
+
+ private IEnumerable ResolveDynamicControllerAssignments(IGamepadDriver gamepadDriver, InputConfig config)
+ {
+ if (gamepadDriver == null)
+ {
+ yield break;
+ }
+
+ List assignedControllers = _playerInputAssignment?.Devices
+ .Where(device => device.Type == AssignedInputDeviceType.Controller)
+ .ToList() ?? [];
+
+ if (_playerInputAssignment?.EnableDynamicInputSwap == true)
+ {
+ foreach (AssignedInputDevice assignedController in assignedControllers)
+ {
+ for (int index = 0; index < gamepadDriver.GamepadsIds.Length; index++)
+ {
+ string gamepadId = gamepadDriver.GamepadsIds[index];
+ if (string.Equals(gamepadId, assignedController.Id, StringComparison.Ordinal))
+ {
+ yield return assignedController;
+ break;
+ }
+ }
}
- VibrationValue leftVibrationValue = dualVibrationValue.Item1;
- VibrationValue rightVibrationValue = dualVibrationValue.Item2;
+ yield break;
+ }
- leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
- leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
- rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
- rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
-
- if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
+ if (config is StandardControllerInputConfig)
+ {
+ for (int index = 0; index < gamepadDriver.GamepadsIds.Length; index++)
{
- float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15)));
- float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85)));
- _gamepad?.Rumble(low, high, 0xFFFFFFFF);
+ string gamepadId = gamepadDriver.GamepadsIds[index];
+ if (string.Equals(gamepadId, config.Id, StringComparison.Ordinal))
+ {
+ yield return new AssignedInputDevice
+ {
+ Type = AssignedInputDeviceType.Controller, Id = gamepadId,
+ };
+ yield break;
+ }
+ }
+ }
+
+ if (!gamepadDriver.GamepadsIds.IsEmpty)
+ {
+ yield return new AssignedInputDevice
+ {
+ Type = AssignedInputDeviceType.Controller,
+ Id = gamepadDriver.GamepadsIds[0],
+ };
+ }
+ }
+
+ private static IGamepad OpenSingleGamepad(IGamepadDriver driver, string id, bool keyboard)
+ {
+ if (driver == null || string.IsNullOrEmpty(id))
+ {
+ return null;
+ }
+
+ if (keyboard && driver is IKeyboardModeDriver keyboardModeDriver)
+ {
+ return keyboardModeDriver.GetKeyboard(id, KeyboardInputMode.Physical);
+ }
+
+ return driver.GetGamepad(id);
+ }
+
+ private void UpdateDynamicConfigurations(InputConfig config)
+ {
+ if (config is StandardKeyboardInputConfig keyboardConfig)
+ {
+ AssignedInputDevice assignedKeyboard = _playerInputAssignment?.Devices.FirstOrDefault(device => device.Type == AssignedInputDeviceType.Keyboard);
+
+ _keyboardConfig = ResolveKeyboardConfiguration(assignedKeyboard, keyboardConfig, _keyboardGamepad);
+
+ _assignedControllerConfigs.Clear();
+
+ foreach (IGamepad controllerGamepad in _assignedControllerGamepads)
+ {
+ AssignedInputDevice assignedController = _playerInputAssignment?.Devices.FirstOrDefault(device =>
+ device.Type == AssignedInputDeviceType.Controller &&
+ device.Id == controllerGamepad.Id);
+
+ _assignedControllerConfigs.Add(ResolveControllerConfiguration(assignedController, keyboardConfig, controllerGamepad));
}
- Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
- // Value=value/multiplier * multiplier (result)
- $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
- $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " +
- $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
- $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " +
- $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " +
- $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
- $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " +
- $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})");
+ _controllerConfig = _assignedControllerConfigs.FirstOrDefault();
+ }
+ else if (config is StandardControllerInputConfig controllerConfig)
+ {
+ _assignedControllerConfigs.Clear();
+
+ foreach (IGamepad controllerGamepad in _assignedControllerGamepads)
+ {
+ AssignedInputDevice assignedController = _playerInputAssignment?.Devices.FirstOrDefault(device =>
+ device.Type == AssignedInputDeviceType.Controller &&
+ device.Id == controllerGamepad.Id);
+
+ _assignedControllerConfigs.Add(ResolveControllerConfiguration(assignedController, controllerConfig, controllerGamepad));
+ }
+
+ _controllerConfig = _assignedControllerConfigs.FirstOrDefault() ?? controllerConfig;
+
+ if (_keyboardGamepad != null)
+ {
+ AssignedInputDevice assignedKeyboard = _playerInputAssignment?.Devices.FirstOrDefault(device => device.Type == AssignedInputDeviceType.Keyboard);
+
+ _keyboardConfig = ResolveKeyboardConfiguration(assignedKeyboard, controllerConfig, _keyboardGamepad);
+ }
+ else
+ {
+ _keyboardConfig = null;
+ }
+ }
+ }
+
+ private StandardKeyboardInputConfig ResolveKeyboardConfiguration(AssignedInputDevice assignedKeyboard, InputConfig baseConfig, IGamepad keyboardGamepad)
+ {
+ if (keyboardGamepad == null)
+ {
+ return null;
+ }
+
+ if (TryLoadAssignedProfile(assignedKeyboard, KeyboardString, keyboardGamepad, baseConfig, out StandardKeyboardInputConfig profileConfig))
+ {
+ return profileConfig;
+ }
+
+ if (baseConfig is StandardKeyboardInputConfig keyboardBaseConfig)
+ {
+ StandardKeyboardInputConfig clonedConfig = CloneConfig(keyboardBaseConfig);
+
+ if (clonedConfig != null)
+ {
+ clonedConfig.Id = keyboardGamepad.Id;
+ clonedConfig.Name = keyboardGamepad.Name;
+ clonedConfig.PlayerIndex = baseConfig.PlayerIndex;
+ clonedConfig.EnableDynamicGamepadSwap = true;
+ return clonedConfig;
+ }
+ }
+
+ StandardKeyboardInputConfig defaultConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
+ keyboardGamepad.Id,
+ keyboardGamepad.Name,
+ baseConfig.ControllerType,
+ baseConfig.PlayerIndex);
+ defaultConfig.EnableDynamicGamepadSwap = true;
+ return defaultConfig;
+ }
+
+ private StandardControllerInputConfig ResolveControllerConfiguration(AssignedInputDevice assignedController, InputConfig baseConfig, IGamepad controllerGamepad)
+ {
+ if (controllerGamepad == null)
+ {
+ return null;
+ }
+
+ if (TryLoadAssignedProfile(assignedController, ControllerString, controllerGamepad, baseConfig, out StandardControllerInputConfig profileConfig))
+ {
+ return profileConfig;
+ }
+
+ if (baseConfig is StandardControllerInputConfig controllerBaseConfig)
+ {
+ StandardControllerInputConfig clonedConfig = CloneConfig(controllerBaseConfig);
+
+ if (clonedConfig != null)
+ {
+ clonedConfig.Id = controllerGamepad.Id;
+ clonedConfig.Name = controllerGamepad.Name;
+ clonedConfig.PlayerIndex = baseConfig.PlayerIndex;
+ clonedConfig.EnableDynamicGamepadSwap = true;
+ return clonedConfig;
+ }
+ }
+
+ StandardControllerInputConfig defaultConfig = InputConfigDefaults.CreateDefaultControllerConfiguration(
+ controllerGamepad.Id,
+ controllerGamepad.Name,
+ baseConfig.ControllerType,
+ baseConfig.PlayerIndex,
+ controllerGamepad.Name?.Contains("Nintendo") == true);
+ defaultConfig.EnableDynamicGamepadSwap = true;
+ return defaultConfig;
+ }
+
+ private static T CloneConfig(T config) where T : InputConfig
+ {
+ return JsonHelper.Deserialize(
+ JsonHelper.Serialize(config, _serializerContext.InputConfig),
+ _serializerContext.InputConfig) as T;
+ }
+
+ private static bool TryLoadAssignedProfile(AssignedInputDevice assignedDevice, string profileDirectory, IGamepad gamepad, InputConfig baseConfig, out T config)
+ where T : InputConfig
+ {
+ config = null;
+
+ if (string.IsNullOrWhiteSpace(assignedDevice?.ProfileName))
+ {
+ return false;
+ }
+
+ string path = Path.Combine(AppDataManager.ProfilesDirPath, profileDirectory, assignedDevice.ProfileName + ".json");
+
+ if (!File.Exists(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig) as T;
+ }
+ catch (JsonException)
+ {
+ return false;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+
+ if (config == null)
+ {
+ return false;
+ }
+
+ config.Id = gamepad.Id;
+ config.Name = gamepad.Name;
+ config.PlayerIndex = baseConfig.PlayerIndex;
+ config.EnableDynamicGamepadSwap = true;
+ return true;
+ }
+
+ private void UpdateDynamic()
+ {
+ GamepadStateSnapshot keyboardState = _keyboardGamepad?.GetMappedStateSnapshot() ?? default;
+ bool keyboardHasInput = _keyboardGamepad != null && HasInput(keyboardState);
+ bool keyboardNewInput = _keyboardGamepad != null && HasNewInput(keyboardState, _previousKeyboardState);
+ int controllerWithNewInput = -1;
+ int controllerWithHeldInput = -1;
+
+ // Note: dynamic swap is "last input wins", so we scan every assigned controller
+ // and promote whichever one most recently produced a meaningful state change.
+ for (int i = 0; i < _assignedControllerGamepads.Count; i++)
+ {
+ IGamepad controllerGamepad = _assignedControllerGamepads[i];
+ GamepadStateSnapshot controllerState = controllerGamepad?.GetMappedStateSnapshot() ?? default;
+
+ if (HasNewInput(controllerState, _previousControllerStates[i]))
+ {
+ controllerWithNewInput = i;
+ }
+
+ if (controllerWithHeldInput == -1 && HasInput(controllerState))
+ {
+ controllerWithHeldInput = i;
+ }
+
+ _previousControllerStates[i] = controllerState;
+ }
+
+ if (keyboardNewInput && controllerWithNewInput == -1)
+ {
+ _activeInputSource = DynamicInputSource.Keyboard;
+ }
+ else if (controllerWithNewInput != -1 && !keyboardNewInput)
+ {
+ _activeInputSource = DynamicInputSource.Controller;
+ _activeControllerIndex = controllerWithNewInput;
+ }
+ else if (_activeInputSource == DynamicInputSource.Keyboard && !keyboardHasInput && controllerWithHeldInput != -1)
+ {
+ _activeInputSource = DynamicInputSource.Controller;
+ _activeControllerIndex = controllerWithHeldInput;
+ }
+ else if (_activeInputSource == DynamicInputSource.Controller && controllerWithHeldInput == -1 && keyboardHasInput)
+ {
+ _activeInputSource = DynamicInputSource.Keyboard;
+ }
+ else if (_activeInputSource == DynamicInputSource.None)
+ {
+ _activeInputSource = _config switch
+ {
+ StandardKeyboardInputConfig when _keyboardGamepad != null => DynamicInputSource.Keyboard,
+ StandardControllerInputConfig when _assignedControllerGamepads.Count > 0 => DynamicInputSource.Controller,
+ _ when keyboardHasInput => DynamicInputSource.Keyboard,
+ _ when controllerWithHeldInput != -1 => DynamicInputSource.Controller,
+ _ when _keyboardGamepad != null => DynamicInputSource.Keyboard,
+ _ when _assignedControllerGamepads.Count > 0 => DynamicInputSource.Controller,
+ _ => DynamicInputSource.None,
+ };
+
+ if (_activeInputSource == DynamicInputSource.Controller)
+ {
+ _activeControllerIndex = controllerWithHeldInput != -1 ? controllerWithHeldInput : 0;
+ }
+ }
+
+ UpdateActiveGamepad();
+
+ State = _activeInputSource switch
+ {
+ DynamicInputSource.Keyboard => keyboardState,
+ DynamicInputSource.Controller when _activeControllerIndex >= 0 && _activeControllerIndex < _previousControllerStates.Count => _previousControllerStates[_activeControllerIndex],
+ _ => default,
+ };
+
+ if (_activeConfig is StandardControllerInputConfig controllerConfig && _controllerGamepad != null && _activeInputSource == DynamicInputSource.Controller)
+ {
+ UpdateControllerMotion(_controllerGamepad, controllerConfig);
+ }
+ else
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ }
+
+ _previousKeyboardState = keyboardState;
+ }
+
+ private void UpdateActiveGamepad()
+ {
+ (_gamepad, _activeConfig, GamepadDriver) = _activeInputSource switch
+ {
+ DynamicInputSource.Keyboard => (_keyboardGamepad, _keyboardConfig, _keyboardDriver),
+ DynamicInputSource.Controller =>
+ (
+ _activeControllerIndex >= 0 && _activeControllerIndex < _assignedControllerGamepads.Count
+ ? _assignedControllerGamepads[_activeControllerIndex]
+ : _assignedControllerGamepads.FirstOrDefault(),
+ _activeControllerIndex >= 0 && _activeControllerIndex < _assignedControllerConfigs.Count
+ ? _assignedControllerConfigs[_activeControllerIndex]
+ : _assignedControllerConfigs.FirstOrDefault(),
+ _controllerDriver
+ ),
+ _ => ((IGamepad)null, (InputConfig)null, (IGamepadDriver)null)
+ };
+
+ _controllerGamepad = _gamepad;
+ }
+
+ private void UpdateControllerMotion(IGamepad gamepad, StandardControllerInputConfig controllerConfig)
+ {
+ if (gamepad == null || controllerConfig?.Motion == null || !controllerConfig.Motion.EnableMotion)
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ return;
+ }
+
+ if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
+ {
+ if ((gamepad.Features & GamepadFeaturesFlag.Motion) != 0)
+ {
+ _leftMotionInput ??= new MotionInput();
+ _rightMotionInput ??= new MotionInput();
+
+ Vector3 accelerometer = gamepad.GetMotionData(MotionInputId.Accelerometer);
+ Vector3 gyroscope = gamepad.GetMotionData(MotionInputId.Gyroscope);
+
+ accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
+ gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y);
+
+ _leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
+
+ if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
+ {
+ if (gamepad.Id == "JoyConPair")
+ {
+ Vector3 rightAccelerometer = gamepad.GetMotionData(MotionInputId.SecondAccelerometer);
+ Vector3 rightGyroscope = gamepad.GetMotionData(MotionInputId.SecondGyroscope);
+
+ rightAccelerometer = new Vector3(rightAccelerometer.X, -rightAccelerometer.Z, rightAccelerometer.Y);
+ rightGyroscope = new Vector3(rightGyroscope.X, -rightGyroscope.Z, rightGyroscope.Y);
+
+ _rightMotionInput.Update(rightAccelerometer, rightGyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
+ }
+ else
+ {
+ _rightMotionInput = _leftMotionInput;
+ }
+ }
+ }
+ else
+ {
+ _leftMotionInput = null;
+ _rightMotionInput = null;
+ }
+ }
+ else if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook && controllerConfig.Motion is CemuHookMotionConfigController cemuControllerConfig)
+ {
+ int clientId = (int)controllerConfig.PlayerIndex;
+
+ _cemuHookClient.RegisterClient(clientId, cemuControllerConfig.DsuServerHost, cemuControllerConfig.DsuServerPort);
+ _cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot);
+ _cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _leftMotionInput);
+
+ if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
+ {
+ if (!cemuControllerConfig.MirrorInput)
+ {
+ _cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot);
+ _cemuHookClient.TryGetData(clientId, cemuControllerConfig.AltSlot, out _rightMotionInput);
+ }
+ else
+ {
+ _rightMotionInput = _leftMotionInput;
+ }
+ }
+ }
+ }
+
+ private static bool HasInput(GamepadStateSnapshot state)
+ {
+ for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
+ {
+ if (state.IsPressed(inputId))
+ {
+ return true;
+ }
+ }
+
+ return StickIsActive(state.GetStick(StickInputId.Left)) || StickIsActive(state.GetStick(StickInputId.Right));
+ }
+
+ private static bool HasNewInput(GamepadStateSnapshot current, GamepadStateSnapshot previous)
+ {
+ for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
+ {
+ if (current.IsPressed(inputId) && !previous.IsPressed(inputId))
+ {
+ return true;
+ }
+ }
+
+ return StickBecameActive(current.GetStick(StickInputId.Left), previous.GetStick(StickInputId.Left)) ||
+ StickBecameActive(current.GetStick(StickInputId.Right), previous.GetStick(StickInputId.Right));
+ }
+
+ private static bool StickIsActive((float X, float Y) stick)
+ {
+ const float Threshold = 0.2f;
+
+ return MathF.Abs(stick.X) > Threshold || MathF.Abs(stick.Y) > Threshold;
+ }
+
+ private static bool StickBecameActive((float X, float Y) current, (float X, float Y) previous)
+ {
+ bool currentActive = StickIsActive(current);
+ bool previousActive = StickIsActive(previous);
+
+ return currentActive && (!previousActive || MathF.Abs(current.X - previous.X) > 0.1f || MathF.Abs(current.Y - previous.Y) > 0.1f);
+ }
+
+ private void DisposeOpenedGamepads()
+ {
+ if (!ReferenceEquals(_gamepad, _keyboardGamepad) && !_assignedControllerGamepads.Contains(_gamepad))
+ {
+ _gamepad?.Dispose();
+ }
+
+ _keyboardGamepad?.Dispose();
+
+ foreach (IGamepad controllerGamepad in _assignedControllerGamepads.Distinct())
+ {
+ controllerGamepad?.Dispose();
}
}
}
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index bf483e2a7..fcc527c80 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -37,6 +37,7 @@ namespace Ryujinx.Input.HLE
private List _inputConfig;
private List _requestedInputConfig;
+ private List _playerInputAssignments;
private bool _enableKeyboard;
private bool _enableMouse;
private Switch _device;
@@ -54,6 +55,7 @@ namespace Ryujinx.Input.HLE
_mouseDriver = mouseDriver;
_inputConfig = [];
_requestedInputConfig = [];
+ _playerInputAssignments = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -78,52 +80,108 @@ namespace Ryujinx.Input.HLE
private void HandleOnGamepadDisconnected(string obj)
{
- // Force input reload
+ List requestedInputConfig;
+ List playerInputAssignments;
+ bool enableKeyboard;
+ bool enableMouse;
+
lock (_lock)
{
// Forcibly disconnect any controllers with this ID.
for (int i = 0; i < _controllers.Length; i++)
{
- if (_controllers[i]?.Id == obj)
+ if (_controllers[i]?.HasAssignedControllerId(obj) == true)
{
_controllers[i]?.Dispose();
_controllers[i] = null;
}
}
- ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
+ requestedInputConfig = _requestedInputConfig;
+ playerInputAssignments = _playerInputAssignments;
+ enableKeyboard = _enableKeyboard;
+ enableMouse = _enableMouse;
}
+
+ // Force input reload.
+ ReloadConfiguration(requestedInputConfig, playerInputAssignments, enableKeyboard, enableMouse);
}
- private void HandleOnGamepadConnected(string _)
+ private void HandleOnGamepadConnected(string id)
{
+ List requestedInputConfig;
+ List playerInputAssignments;
+ bool enableKeyboard;
+ bool enableMouse;
+
+ lock (_lock)
+ {
+ for (int i = 0; i < _controllers.Length; i++)
+ {
+ if (_controllers[i] != null && PlayerHasAssignedControllerId((PlayerIndex)i, id))
+ {
+ _controllers[i]?.Dispose();
+ _controllers[i] = null;
+ }
+ }
+
+ requestedInputConfig = _requestedInputConfig;
+ playerInputAssignments = _playerInputAssignments;
+ enableKeyboard = _enableKeyboard;
+ enableMouse = _enableMouse;
+ }
+
// Force input reload
- ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
+ ReloadConfiguration(requestedInputConfig, playerInputAssignments, enableKeyboard, enableMouse);
+ }
+
+ private bool PlayerHasAssignedControllerId(PlayerIndex playerIndex, string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ return false;
+ }
+
+ InputConfig inputConfig = _requestedInputConfig.FirstOrDefault(config => (int)config.PlayerIndex == (int)playerIndex);
+
+ if (inputConfig == null)
+ {
+ return false;
+ }
+
+ PlayerInputAssignment playerInputAssignment = GetPlayerInputAssignment(inputConfig);
+
+ return playerInputAssignment.EnableDynamicInputSwap &&
+ playerInputAssignment.Devices.Any(device =>
+ device.Type == AssignedInputDeviceType.Controller &&
+ string.Equals(device.Id, id, StringComparison.Ordinal));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
+ private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config, PlayerInputAssignment playerInputAssignment)
{
- IGamepadDriver targetDriver =
- config is StandardKeyboardInputConfig
- ? _keyboardDriver
- : _gamepadDriver;
+ Debug.Assert(_keyboardDriver != null, "Keyboard driver is not initialized!");
+ Debug.Assert(_gamepadDriver != null, "Gamepad driver is not initialized!");
- Debug.Assert(targetDriver != null, "Unknown input configuration!");
-
- if (controller.GamepadDriver != targetDriver || controller.Id != config.Id)
+ if (!controller.MatchesDriverConfiguration(config, playerInputAssignment))
{
- return controller.UpdateDriverConfiguration(targetDriver, config);
+ return controller.UpdateDriverConfiguration(_keyboardDriver, _gamepadDriver, config, playerInputAssignment);
}
- return controller.GamepadDriver != null;
+ return controller.IsAvailable;
}
public void ReloadConfiguration(List inputConfig, bool enableKeyboard, bool enableMouse)
+ {
+ ReloadConfiguration(inputConfig, [], enableKeyboard, enableMouse);
+ }
+
+ public void ReloadConfiguration(List inputConfig, List playerInputAssignments, bool enableKeyboard, bool enableMouse)
{
lock (_lock)
{
_requestedInputConfig = inputConfig?.ToList() ?? [];
+ _playerInputAssignments = playerInputAssignments?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray();
@@ -146,14 +204,17 @@ namespace Ryujinx.Input.HLE
}
InputConfig activeConfig = inputConfigEntry;
- bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
+ PlayerInputAssignment playerInputAssignment = GetPlayerInputAssignment(inputConfigEntry);
+
+ bool isValid = DriverConfigurationUpdate(ref controller, activeConfig, playerInputAssignment);
if (!isValid &&
+ !playerInputAssignment.EnableDynamicInputSwap &&
inputConfigEntry is StandardControllerInputConfig &&
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
{
activeConfig = fallbackConfig;
- isValid = DriverConfigurationUpdate(ref controller, activeConfig);
+ isValid = DriverConfigurationUpdate(ref controller, activeConfig, playerInputAssignment);
}
if (!isValid)
@@ -184,6 +245,54 @@ namespace Ryujinx.Input.HLE
}
}
+ private PlayerInputAssignment GetPlayerInputAssignment(InputConfig inputConfig)
+ {
+ PlayerInputAssignment playerInputAssignment = _playerInputAssignments.FirstOrDefault(assignment => assignment.PlayerIndex == inputConfig.PlayerIndex);
+
+ if (playerInputAssignment != null)
+ {
+ PlayerInputAssignment normalizedAssignment = PlayerInputAssignmentHelper.Normalize(
+ playerInputAssignment,
+ PlayerInputAssignmentHelper.CreatePrimaryDevice(inputConfig));
+
+ if (normalizedAssignment.EnableDynamicInputSwap || normalizedAssignment.Devices.Count > 0)
+ {
+ return normalizedAssignment;
+ }
+ }
+
+ // Note: older configs only know about a single saved device per player,
+ // so we synthesize a routing entry here until the user saves explicit assignments.
+ playerInputAssignment = new PlayerInputAssignment
+ {
+ PlayerIndex = inputConfig.PlayerIndex,
+ EnableDynamicInputSwap = inputConfig.EnableDynamicGamepadSwap,
+ };
+
+ AssignedInputDevice primaryDevice = PlayerInputAssignmentHelper.CreatePrimaryDevice(inputConfig);
+
+ if (primaryDevice != null)
+ {
+ playerInputAssignment.Devices.Add(primaryDevice);
+ }
+
+ if (playerInputAssignment.EnableDynamicInputSwap && inputConfig is StandardControllerInputConfig)
+ {
+ string keyboardId = _keyboardDriver.GamepadsIds.IsEmpty ? null : _keyboardDriver.GamepadsIds[0];
+
+ if (!string.IsNullOrWhiteSpace(keyboardId))
+ {
+ playerInputAssignment.Devices.Add(new AssignedInputDevice
+ {
+ Type = AssignedInputDeviceType.Keyboard,
+ Id = keyboardId,
+ });
+ }
+ }
+
+ return playerInputAssignment;
+ }
+
private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
{
fallbackConfig = null;
@@ -210,6 +319,8 @@ namespace Ryujinx.Input.HLE
inputConfig.ControllerType,
inputConfig.PlayerIndex);
+ fallbackConfig.EnableDynamicGamepadSwap = inputConfig.EnableDynamicGamepadSwap;
+
return true;
}
@@ -257,11 +368,16 @@ namespace Ryujinx.Input.HLE
}
public void Initialize(Switch device, List inputConfig, bool enableKeyboard, bool enableMouse)
+ {
+ Initialize(device, inputConfig, [], enableKeyboard, enableMouse);
+ }
+
+ public void Initialize(Switch device, List inputConfig, List playerInputAssignments, bool enableKeyboard, bool enableMouse)
{
_device = device;
_device.Configuration.RefreshInputConfig = RefreshInputConfigForHLE;
- ReloadConfiguration(inputConfig, enableKeyboard, enableMouse);
+ ReloadConfiguration(inputConfig, playerInputAssignments, enableKeyboard, enableMouse);
}
public void Update(float aspectRatio = 1)
@@ -286,7 +402,7 @@ namespace Ryujinx.Input.HLE
// Do we allow input updates and is a controller connected?
if (_inputUpdateBlockCount == 0 && controller != null)
{
- DriverConfigurationUpdate(ref controller, inputConfig);
+ DriverConfigurationUpdate(ref controller, inputConfig, GetPlayerInputAssignment(inputConfig));
controller.UpdateUserConfiguration(inputConfig);
controller.Update();
@@ -387,7 +503,9 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
- return _inputConfig.FirstOrDefault(x => x.PlayerIndex == (Common.Configuration.Hid.PlayerIndex)index);
+ NpadController controller = _controllers[index];
+
+ return controller?.ActiveConfig ?? _inputConfig.FirstOrDefault(x => x.PlayerIndex == (Common.Configuration.Hid.PlayerIndex)index);
}
}
diff --git a/src/Ryujinx.Input/InputConfigDefaults.cs b/src/Ryujinx.Input/InputConfigDefaults.cs
index 5608923f7..7d0332d60 100644
--- a/src/Ryujinx.Input/InputConfigDefaults.cs
+++ b/src/Ryujinx.Input/InputConfigDefaults.cs
@@ -140,7 +140,7 @@ namespace Ryujinx.Input
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
- UseHDRumble = true,
+ UseHDRumble = false
},
};
}
diff --git a/src/Ryujinx.Tests/Cpu/CpuTest.cs b/src/Ryujinx.Tests/Cpu/CpuTest.cs
index c01a9e4e8..aab85a76f 100644
--- a/src/Ryujinx.Tests/Cpu/CpuTest.cs
+++ b/src/Ryujinx.Tests/Cpu/CpuTest.cs
@@ -10,6 +10,8 @@ using MemoryPermission = Ryujinx.Tests.Unicorn.MemoryPermission;
namespace Ryujinx.Tests.Cpu
{
[TestFixture]
+ [Parallelizable(ParallelScope.All)]
+ [FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class CpuTest
{
protected static readonly ulong Size = MemoryBlock.GetPageSize();
diff --git a/src/Ryujinx.Tests/Cpu/CpuTest32.cs b/src/Ryujinx.Tests/Cpu/CpuTest32.cs
index e742c1713..ea78b7844 100644
--- a/src/Ryujinx.Tests/Cpu/CpuTest32.cs
+++ b/src/Ryujinx.Tests/Cpu/CpuTest32.cs
@@ -10,6 +10,8 @@ using MemoryPermission = Ryujinx.Tests.Unicorn.MemoryPermission;
namespace Ryujinx.Tests.Cpu
{
[TestFixture]
+ [Parallelizable(ParallelScope.All)]
+ [FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class CpuTest32
{
protected static readonly uint Size = (uint)MemoryBlock.GetPageSize();
diff --git a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs
index d67f22160..a6014ed56 100644
--- a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs
+++ b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs
@@ -51,7 +51,7 @@ namespace Ryujinx.Tests.Cpu
bool methodCalled = false;
bool isFz = false;
- TranslatorTestMethods.FpFlagsPInvokeTest method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest();
+ TranslatorTestMethods.FpFlagsPInvokeTest method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest(_translator.JitCache);
// This method sets flush-to-zero and then calls the managed method.
// Before and after setting the flags, it ensures subnormal addition works as expected.
diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs
index c2eeded4d..f80a7519f 100644
--- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs
+++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs
@@ -245,7 +245,7 @@ namespace Ryujinx.Tests.Memory
// Create a large mapping.
mainMemory.MapView(backing, 0, 0, vaSize);
- TestMethods.DebugNativeWriteLoop writeFunc = TestMethods.GenerateDebugNativeWriteLoop();
+ TestMethods.DebugNativeWriteLoop writeFunc = TestMethods.GenerateDebugNativeWriteLoop(_translator.JitCache);
nint writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4);
Thread testThread = new(() =>
@@ -339,7 +339,7 @@ namespace Ryujinx.Tests.Memory
fixed (void* localMap = &state.LocalCounts)
{
- TestMethods.DebugThreadLocalMapGetOrReserve getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((nint)localMap);
+ TestMethods.DebugThreadLocalMapGetOrReserve getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve(_translator.JitCache, (nint)localMap);
for (int i = 0; i < ThreadLocalMap.MapSize; i++)
{
diff --git a/src/Ryujinx/Assets/Splashes.json b/src/Ryujinx/Assets/Splashes.json
index 0f81f15af..a08a6fa3a 100644
--- a/src/Ryujinx/Assets/Splashes.json
+++ b/src/Ryujinx/Assets/Splashes.json
@@ -1,7 +1,37 @@
{
"Locales": {
"ar_SA": [],
- "de_DE": [],
+ "de_DE": [
+ "Ryubing ist mein zweiter Vorname.",
+ "Hier schlechten Witz einfügen.",
+ "ES IST RYUBING-ZEIT!",
+ "Fantastisch!",
+ "Jetzt mit 100% mehr Humor!",
+ "Jetzt geht es erst richtig los...",
+ "War wohl nur der Wind...",
+ "Kann Spuren von Nüssen enthalten!",
+ "Aus 100% biologischem Anbau!",
+ "Schönen Tag noch! : )",
+ "Der Weltraum... unendliche Weiten!",
+ "Falls du es noch nicht bemerkt hast: Ich liebe es schlechte Witze zu machen : )",
+ "In C# programmiert!",
+ "Forgejo hat Gitlab entthront!",
+ "Irgendwelche Ideen, was hier stehen könnte?",
+ "Guten Morgen!",
+ "Guten Tag!",
+ "Guten Abend!",
+ "Ich hoffe, du hast einen tollen Tag!",
+ "ICH... BIN RYUBING!",
+ "Nein, Lossless Scaling wird NICHT unterstützt.",
+ "Trans-Rechte sind Menschenrechte!",
+ ":3",
+ "Bitte einen Controller anschließen!",
+ "Never gonna give you up!",
+ "Ganon beobachtet dich!",
+ "Unzählige Stunden voller Spaß!",
+ "Yahaha! Du hast mich endeckt!",
+ "Finde uns auf BlueSky"
+ ],
"el_GR": [],
"en_US": [
"Ryubing is my middle name.",
@@ -76,10 +106,90 @@
"Whaaa? You came to see me again? That makes Beedle SO HAPPY!",
"Don't get cooked, stay off the hook!",
"Now with 100% more good vibes in the splash code!",
- "It is Wednesday my dudes!"
+ "It is Wednesday my dudes!",
+ "Nifty",
+ "Find us on BlueSky",
+ "We are the knights who say... Ni."
],
"es_ES": [],
- "fr_FR": [],
+ "fr_FR": [
+ "Ryubing est mon deuxième prénom.",
+ "Toujours à 110% !",
+ "Je ne pense pas donc je ne suis pas !",
+ "Tout gloire à Egg.",
+ "Insérer une blague nulle ici.",
+ "C'EST L'HEURE DE RYUBING !",
+ "Je déteste les lundis...",
+ "Fantastique !",
+ "Maintenant avec 100% d'humour en plus !",
+ "'Non approuvé par S&P' a été approuvé par S&P.",
+ "N'ÊTES-VOUS PAS DIVERTI ?",
+ "C'est un émulateur !",
+ "Maintenant le vrai jeu commence...",
+ "Cuit frais depuis 2018 !",
+ "Ça devait être le vent...",
+ "J'étais un aventurier comme toi avant de recevoir une flèche dans le genou.",
+ "Ryubing !",
+ "Peut contenir des fruits à coque !",
+ "Peut inclure des références occasionnelles à la pop culture !",
+ "100% issue d'agriculture durable !",
+ "Passez une bonne journée : )",
+ "Texte du bas",
+ "Je suis désolé Dave. Je crains que je ne puisse pas faire ça.",
+ "Ce n'est pas une lune...",
+ "Monsieur, en train de terminer ce combat.",
+ "Je vois comment c'est...",
+ "L'espace ! La frontière finale !",
+ "Si vous ne l'aviez pas déjà deviné, j'adore faire de mauvaises blagues : )",
+ "ceci.",
+ "cela",
+ "Probablement ne contient pas de haricots cuits.",
+ "Vous êtes prêts ?",
+ "Herobrine supprimé.",
+ "Droit à la réparation !",
+ "Programmé en C#!",
+ "Forgejo a détrôné Gitlab !",
+ "Des idées quoi mettre ici ?",
+ "Bonjour !",
+ "Bon après-midi !",
+ "Bonne soirée !",
+ "J'espère que vous passez une excellente journée !",
+ "Veuillez insérer le disque deux !",
+ "Je... SUIS RYUBING !",
+ "Ryubing toujours à fond",
+ "bing bing wahoo.",
+ "œuf",
+ "Non, le scaling sans perte n'est PAS supporté.",
+ "Comment le faites-vous ?",
+ "Un euro.",
+ "Deux euros",
+ "Quelqu'un m'a dit un jour !",
+ "C'est encore cette période de l'année !",
+ "Infusé uniquement avec les meilleures blagues.",
+ "La compilation de shaders asynchrone détruirait mon âme : (",
+ "Les droits trans sont des droits humains !",
+ ":3",
+ ":3 prohibé",
+ "Bug de réplication du splash ':3' corrigé.",
+ "Veuillez connecter une manette !",
+ "Never gonna give you up !",
+ "Le jeu était donc truqué depuis le début...",
+ "Ganon vous observe !",
+ "Maintenant avec 100% de JSON en plus dans le code des splash !",
+ "D'innombrables heures de plaisir !",
+ "Désolé, Link. Je ne peux pas donner de crédit. Revenez quand vous serez un peu... mmmmmm... plus riche !",
+ "Vous avez rencontré un terrible destin, n'est-ce pas ?",
+ "Yahaha ! Tu m'as trouvé !",
+ "J'aurais été dans de beaux draps si tu n'étais pas intervenu à ce moment-là, goro.",
+ "Restez au frais !",
+ "Jaune, noir. Jaune, noir. Jaune, noir. Jaune, noir. Oh, noir et jaune ! Secouons un peu ça.",
+ "Quoi ? Tu es venu me revoir ? Ça rend Beedle TROP HEUREUX !",
+ "Ne vous faites pas cuire, restez hors du crochet !",
+ "Maintenant avec 100% de bonnes vibes en plus dans le code des splash !",
+ "Eh oui jamy !",
+ "Allez Marcel !",
+ "Retrouvez nous sur BlueSky"
+ ],
"he_IL": [],
"it_IT": [],
"ja_JP": [],
diff --git a/src/Ryujinx/Common/Models/XCITrimmerFileModel.cs b/src/Ryujinx/Common/Models/XCITrimmerFileModel.cs
index 233ac2b6d..3362d063c 100644
--- a/src/Ryujinx/Common/Models/XCITrimmerFileModel.cs
+++ b/src/Ryujinx/Common/Models/XCITrimmerFileModel.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
@@ -11,6 +12,7 @@ namespace Ryujinx.Ava.Common.Models
bool Untrimmable,
long PotentialSavingsB,
long CurrentSavingsB,
+ long OriginalSizeB,
int? PercentageProgress,
XCIFileTrimmer.OperationOutcome ProcessingOutcome)
{
@@ -25,31 +27,57 @@ namespace Ryujinx.Ava.Common.Models
trimmer.CanBeUntrimmed,
trimmer.DiskSpaceSavingsB,
trimmer.DiskSpaceSavedB,
+ applicationData.FileSize,
null,
XCIFileTrimmer.OperationOutcome.Undetermined
);
}
- public bool IsFailed
+ public bool IsFailed =>
+ ProcessingOutcome is not XCIFileTrimmer.OperationOutcome.Undetermined
+ and not XCIFileTrimmer.OperationOutcome.Successful;
+
+ public string StatusText
{
get
{
- return ProcessingOutcome is not XCIFileTrimmer.OperationOutcome.Undetermined and
- not XCIFileTrimmer.OperationOutcome.Successful;
+ if (IsFailed)
+ return LocaleManager.Instance[LocaleKeys.XCITrimmer_FailedLabel];
+
+ return ProcessingOutcome switch
+ {
+ XCIFileTrimmer.OperationOutcome.Successful =>
+ CurrentSavingsB > 0
+ ? LocaleManager.Instance[LocaleKeys.XCITrimmer_UntrimmedLabel]
+ : LocaleManager.Instance[LocaleKeys.XCITrimmer_TrimmedLabel],
+
+ XCIFileTrimmer.OperationOutcome.Undetermined =>
+ Trimmable && Untrimmable
+ ? LocaleManager.Instance[LocaleKeys.XCITrimmer_PartialLabel]
+
+ : Trimmable
+ ? LocaleManager.Instance[LocaleKeys.XCITrimmer_UntrimmedLabel]
+
+ : Untrimmable
+ ? LocaleManager.Instance[LocaleKeys.XCITrimmer_TrimmedLabel]
+
+ : LocaleManager.Instance[LocaleKeys.XCITrimmer_UnknownLabel],
+
+ _ => LocaleManager.Instance[LocaleKeys.XCITrimmer_UnknownLabel]
+ };
}
}
+ public bool HasStatusDetail =>
+ ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Undetermined;
+
public virtual bool Equals(XCITrimmerFileModel obj)
{
- if (obj == null)
+ if (obj is null)
return false;
- return this.Path == obj.Path;
- }
-
- public override int GetHashCode()
- {
- return this.Path.GetHashCode();
+ return Path == obj.Path;
}
+ public override int GetHashCode() => Path.GetHashCode();
}
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx/Common/SplashTextHelper.cs b/src/Ryujinx/Common/SplashTextHelper.cs
index 9889a225d..e15ec1a27 100644
--- a/src/Ryujinx/Common/SplashTextHelper.cs
+++ b/src/Ryujinx/Common/SplashTextHelper.cs
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using Ryujinx.Common.Logging;
using Gommon;
using Ryujinx.Ava.Systems.Configuration;
-using System;
using System.Text.Json;
namespace Ryujinx.Common
@@ -11,47 +10,48 @@ namespace Ryujinx.Common
{
public static void PrintSplash()
{
+ string splash = GetSplash();
+
Logger.Notice.Print(LogClass.Application, " ___ __ _ ");
Logger.Notice.Print(LogClass.Application, @" / _ \ __ __ __ __ / / (_) ___ ___ _");
Logger.Notice.Print(LogClass.Application, @" / , _/ / // // // / / _ \ / / / _ \ / _ `/");
Logger.Notice.Print(LogClass.Application, @"/_/|_| \_, / \_,_/ /_.__//_/ /_//_/ \_, / ");
Logger.Notice.Print(LogClass.Application, " /___/ /___/ ");
- Logger.Notice.Print(LogClass.Application, "");
- Logger.Notice.Print(LogClass.Application, GetSplash());
- Logger.Notice.Print(LogClass.Application, "");
+
+ if (splash is null)
+ {
+ Logger.Error?.Print(LogClass.Application, "Failed to fetch Splash Text! Splash JSON is invalid!");
+ return;
+ }
+
+ if (!splash.IsNullOrEmpty())
+ {
+ Logger.Notice.Print(LogClass.Application, "");
+ Logger.Notice.Print(LogClass.Application, splash);
+ Logger.Notice.Print(LogClass.Application, "");
+ }
}
- private static string s_finalSplash = "";
+ private static string _finalSplash;
public static string GetSplash()
{
- if (string.IsNullOrEmpty(s_finalSplash))
+ if (_finalSplash is null)
{
- s_finalSplash = GetLangJson();
- if (string.IsNullOrEmpty(s_finalSplash))
+ try
{
- s_finalSplash = "Splash Text";
+ string data;
+ data = EmbeddedResources.ReadAllText("Ryujinx/Assets/Splashes.json");
+ SplashLocales splashJson = JsonSerializer.Deserialize(data);
+ _finalSplash = splashJson.Locales[ConfigurationState.Instance.UI.LanguageCode.Value].GetRandomElement() ?? "";
+ }
+ catch
+ {
+ return null;
}
}
-
- return $"{s_finalSplash}";
- }
-
- private static SplashLocales s_splashJson;
-
- private static string GetLangJson()
- {
- try
- {
- string data;
- data = EmbeddedResources.ReadAllText("Ryujinx/Assets/Splashes.json");
- s_splashJson = JsonSerializer.Deserialize(data);
- return s_splashJson.Locales[ConfigurationState.Instance.UI.LanguageCode.Value].GetRandomElement();
- }
- catch
- {
- return "";
- }
+
+ return _finalSplash;
}
private struct SplashLocales
diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs
index 23f6d6b47..650ea5ae9 100644
--- a/src/Ryujinx/Systems/AppHost.cs
+++ b/src/Ryujinx/Systems/AppHost.cs
@@ -468,11 +468,11 @@ namespace Ryujinx.Ava.Systems
if (ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig)
{
- NpadManager.Initialize(Device, ConfigurationState.InstanceExtra.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ NpadManager.Initialize(Device, ConfigurationState.InstanceExtra.Hid.InputConfig, ConfigurationState.InstanceExtra.Hid.PlayerInputAssignments, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
}
else
{
- NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.PlayerInputAssignments, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
}
TouchScreenManager.Initialize(Device);
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
index 55b2d2cac..7928a6194 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
@@ -426,6 +426,16 @@ namespace Ryujinx.Ava.Systems.Configuration
///
public List InputConfig { get; set; }
+ ///
+ /// Player-level input routing assignments
+ ///
+ public List PlayerInputAssignments { get; set; }
+
+ ///
+ /// Whether to allow mapping the same input device to multiple players.
+ ///
+ public bool AllowDuplicateDeviceAssignment { get; set; }
+
///
/// The speed of spectrum cycling for the Rainbow LED feature.
///
@@ -492,15 +502,20 @@ namespace Ryujinx.Ava.Systems.Configuration
public bool DebuggerSuspendOnStart { get; set; }
///
- /// Show toggles for dirty hacks in the UI.
+ /// Show toggles for dirty hacks in the UI.
///
public bool ShowDirtyHacks { get; set; }
///
- /// The packed values of the enabled dirty hacks.
+ /// The packed values of the enabled dirty hacks.
///
public ulong[] DirtyHacks { get; set; }
+ ///
+ /// Pause emulation while scanning Amiibo
+ ///
+ public bool PauseEmulationWhileScanningAmiibo { get; set; }
+
///
/// Loads a configuration file from disk
///
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
index 3fd132cff..59b1f5804 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
@@ -115,7 +115,7 @@ namespace Ryujinx.Ava.Systems.Configuration
System.SkipUserProfilesManager.Value = cff.SkipUserProfiles;
System.UseHypervisor.Value = cff.UseHypervisor;
System.GCLowLatency.Value = cff.GCLowLatency;
-
+
UI.GuiColumns.FavColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
UI.GuiColumns.IconColumn.Value = shouldLoadFromFile ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
UI.GuiColumns.AppColumn.Value = shouldLoadFromFile ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value;
@@ -151,12 +151,15 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.WindowStartup.WindowPositionX.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value;
UI.WindowStartup.WindowPositionY.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value;
UI.WindowStartup.WindowMaximized.Value = shouldLoadFromFile ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value;
+ UI.PauseEmulationWhileScanningAmiibo.Value = cff.PauseEmulationWhileScanningAmiibo;
Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse;
Hid.DisableInputWhenOutOfFocus.Value = shouldLoadFromFile ? cff.DisableInputWhenOutOfFocus : Hid.DisableInputWhenOutOfFocus.Value; // Get from global config only
Hid.Hotkeys.Value = shouldLoadFromFile ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only
Hid.InputConfig.Value = cff.InputConfig ?? [] ;
+ Hid.PlayerInputAssignments.Value = cff.PlayerInputAssignments ?? [];
+ Hid.AllowDuplicateDeviceAssignment.Value = cff.AllowDuplicateDeviceAssignment;
Hid.RainbowSpeed.Value = cff.RainbowSpeed;
Multiplayer.LanInterfaceId.Value = cff.MultiplayerLanInterfaceId;
@@ -336,7 +339,7 @@ namespace Ryujinx.Ava.Systems.Configuration
EnableRumble = false,
StrongRumble = 1f,
WeakRumble = 1f,
- UseHDRumble = true
+ UseHDRumble = false
};
}
}
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
index c66f74ed5..6af7a2383 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
@@ -191,6 +191,16 @@ namespace Ryujinx.Ava.Systems.Configuration
///
public ReactiveObject IsAscendingOrder { get; private set; }
+ ///
+ /// Show Dynamic Input Swap first-use warning
+ ///
+ public ReactiveObject ShowDynamicInputSwapWarning { get; private set; }
+
+ ///
+ /// Pause emulation while scanning Amiibo
+ ///
+ public ReactiveObject PauseEmulationWhileScanningAmiibo { get; private set; }
+
public UISection()
{
GuiColumns = new Columns();
@@ -210,6 +220,10 @@ namespace Ryujinx.Ava.Systems.Configuration
LanguageCode = new ReactiveObject();
ShowConsole = new ReactiveObject();
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
+ ShowDynamicInputSwapWarning = new ReactiveObject();
+ ShowDynamicInputSwapWarning.Value = true;
+ PauseEmulationWhileScanningAmiibo = new ReactiveObject();
+ PauseEmulationWhileScanningAmiibo.Value = true;
}
}
@@ -513,6 +527,19 @@ namespace Ryujinx.Ava.Systems.Configuration
///
public ReactiveObject> InputConfig { get; private set; }
+ ///
+ /// Player-level input routing assignments.
+ /// NOTE: This keeps dynamic input swap and multi-device ownership attached to the player,
+ /// not to the currently edited keyboard/controller profile.
+ ///
+ public ReactiveObject> PlayerInputAssignments { get; private set; }
+
+ ///
+ /// Whether to allow mapping the same input device to multiple players.
+ /// This is a global setting shared across all players.
+ ///
+ public ReactiveObject AllowDuplicateDeviceAssignment { get; private set; }
+
///
/// The speed of spectrum cycling for the Rainbow LED feature.
///
@@ -525,6 +552,8 @@ namespace Ryujinx.Ava.Systems.Configuration
DisableInputWhenOutOfFocus = new ReactiveObject();
Hotkeys = new ReactiveObject();
InputConfig = new ReactiveObject>();
+ PlayerInputAssignments = new ReactiveObject>();
+ AllowDuplicateDeviceAssignment = new ReactiveObject();
RainbowSpeed = new ReactiveObject();
RainbowSpeed.Event += (_, args) => Rainbow.Speed = args.NewValue;
}
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
index acf8a6793..bcd8e50ca 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
@@ -143,6 +143,8 @@ namespace Ryujinx.Ava.Systems.Configuration
DisableInputWhenOutOfFocus = Hid.DisableInputWhenOutOfFocus,
Hotkeys = Hid.Hotkeys,
InputConfig = Hid.InputConfig,
+ PlayerInputAssignments = Hid.PlayerInputAssignments,
+ AllowDuplicateDeviceAssignment = Hid.AllowDuplicateDeviceAssignment,
RainbowSpeed = Hid.RainbowSpeed,
GraphicsBackend = Graphics.GraphicsBackend,
PreferredGpu = Graphics.PreferredGpu,
@@ -156,6 +158,7 @@ namespace Ryujinx.Ava.Systems.Configuration
DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart,
ShowDirtyHacks = Hacks.ShowDirtyHacks,
DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(),
+ PauseEmulationWhileScanningAmiibo = UI.PauseEmulationWhileScanningAmiibo,
};
return configurationFile;
@@ -260,6 +263,7 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.WindowStartup.WindowPositionX.Value = 0;
UI.WindowStartup.WindowPositionY.Value = 0;
UI.WindowStartup.WindowMaximized.Value = false;
+ UI.PauseEmulationWhileScanningAmiibo.Value = true;
Hid.EnableKeyboard.Value = false;
Hid.EnableMouse.Value = false;
Hid.DisableInputWhenOutOfFocus.Value = false;
@@ -332,6 +336,22 @@ namespace Ryujinx.Ava.Systems.Configuration
},
}
];
+ Hid.PlayerInputAssignments.Value =
+ [
+ new PlayerInputAssignment
+ {
+ PlayerIndex = PlayerIndex.Player1,
+ EnableDynamicInputSwap = false,
+ Devices =
+ [
+ new AssignedInputDevice
+ {
+ Type = AssignedInputDeviceType.Keyboard,
+ Id = "0",
+ },
+ ],
+ },
+ ];
Debug.EnableGdbStub.Value = false;
Debug.GdbStubPort.Value = 55555;
Debug.DebuggerSuspendOnStart.Value = false;
diff --git a/src/Ryujinx/Systems/DiscordIntegrationModule.cs b/src/Ryujinx/Systems/DiscordIntegrationModule.cs
index 195840133..38b2c174c 100644
--- a/src/Ryujinx/Systems/DiscordIntegrationModule.cs
+++ b/src/Ryujinx/Systems/DiscordIntegrationModule.cs
@@ -9,7 +9,9 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
+using System.Collections.Generic;
using System.Text;
+using System.Text.Json;
namespace Ryujinx.Ava.Systems
{
@@ -135,20 +137,48 @@ namespace Ryujinx.Ava.Systems
if (!formattedValue.Handled)
return;
+
- _discordPresencePlaying.Details = TruncateToByteLength(
- formattedValue.Reset
- ? $"Playing {_currentApp.Title}"
- : formattedValue.FormattedString
- );
+
+ try // New format that attempts to deserialize json, and if it fails (using old method)...
+ {
+ Dictionary outDictionary = JsonSerializer.Deserialize>(formattedValue.FormattedString);
+
+ _discordPresencePlaying.Details = TruncateToByteLength(
+ outDictionary["Details"].IsNullOrEmpty()
+ ? $"Playing {_currentApp.Title}"
+ : outDictionary["Details"]
+ );
+
+ _discordPresencePlaying.State = TruncateToByteLength(
+ outDictionary["State"].IsNullOrEmpty()
+ ? $"Total play time: {ValueFormatUtils.FormatTimeSpan(_currentApp.TimePlayed)}"
+ : outDictionary["State"]
+ );
- if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
+ }
+ catch // Utilize original code
+ {
+ _discordPresencePlaying.Details = TruncateToByteLength(
+ formattedValue.Reset
+ ? $"Playing {_currentApp.Title}"
+ : formattedValue.FormattedString
+ );
+ }
+
+ if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details) && _discordClient.CurrentPresence.State.Equals(_discordPresencePlaying.State))
return; //don't trigger an update if the set presence Details are identical to current
_discordClient.SetPresence(_discordPresencePlaying);
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
}
+ public static string PrepareMultilineRpcString(string line1 = "", string line2 = "")
+ {
+ Dictionary rpcdict = new() { { "Details", line1 }, {"State", line2} };
+ return JsonSerializer.Serialize(rpcdict);
+ }
+
private static string TruncateToByteLength(string input)
{
if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit)
diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs
index 043eb1bdc..3ff493b45 100644
--- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs
+++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs
@@ -1105,7 +1105,7 @@ namespace Ryujinx.Ava.Systems.PlayReport
return $"At the main menu";
- static string LocFinal(string? location) => location switch
+ static string LocFinal(string location) => location switch
{
"0" => "Somewhere in Miitopia",
"1" => "Wandering around Greenhorne",
@@ -1151,8 +1151,10 @@ namespace Ryujinx.Ava.Systems.PlayReport
{
return "At the main menu";
}
-
- static string MarioOrLuigiGamemode(string? gamemode) => gamemode switch
+
+ return "";
+
+ static string MarioOrLuigiGamemode(string gamemode) => gamemode switch
{
"0" => "mario",
"1" => "luigi",
@@ -1161,14 +1163,14 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => gamemode
};
- static string OtherGameMode(string? gamemode) => gamemode switch
- {
- "2" => "Boost Rush",
- "3" => "Challenges",
- "4" => "Coin Battle",
- "5" => "Coin Battle Editor",
- _ => ""
- };
+ // static string OtherGameMode(string gamemode) => gamemode switch
+ // {
+ // "2" => "Boost Rush",
+ // "3" => "Challenges",
+ // "4" => "Coin Battle",
+ // "5" => "Coin Battle Editor",
+ // _ => ""
+ // };
static string SpecialMapNames(int? course) => course switch
{
@@ -1196,9 +1198,6 @@ namespace Ryujinx.Ava.Systems.PlayReport
// Glacier seals = 16, water leaf = 15
// desert ice = 14, acorn squid = 13
// all other course numbers are to be considered a hazard
-
- return "";
-
}
}
}
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 45235ee3f..9c28be6e8 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -187,7 +187,7 @@ namespace Ryujinx.Ava.UI.Applet
StringLengthMax = 25
};
(UserResult result, string userInput) =
- await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.CabinetDialog], args);
+ await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_RenameAmiiboTitle], args);
if (result == UserResult.Ok)
{
inputText = userInput;
@@ -212,11 +212,11 @@ namespace Ryujinx.Ava.UI.Applet
{
dialogCloseEvent.Set();
await ContentDialogHelper.CreateInfoDialog(
- LocaleManager.Instance[LocaleKeys.CabinetScanDialog],
+ LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboMessage],
string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
string.Empty,
- LocaleManager.Instance[LocaleKeys.CabinetTitle]
+ LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboTitle]
);
});
dialogCloseEvent.WaitOne();
diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs
index bef6fc47d..9e7572e1a 100644
--- a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs
+++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs
@@ -74,10 +74,14 @@ namespace Ryujinx.Ava.UI.Controls
content._host = contentDialog;
contentDialog.Title = title;
- contentDialog.PrimaryButtonText = args.SubmitText;
+
+ contentDialog.PrimaryButtonText = string.IsNullOrWhiteSpace(args.SubmitText)
+ ? LocaleManager.Instance[LocaleKeys.InputDialogOk]
+ : args.SubmitText;
+
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
contentDialog.SecondaryButtonText = string.Empty;
- contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogCancel];
+ contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.Common_Buttons_Cancel];
contentDialog.Content = content;
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
index 9f2999a4c..e8d028f5d 100755
--- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml
@@ -103,10 +103,10 @@
+ IsVisible="{Binding IsXCIFile}"
+ Icon="{ext:Icon fa-solid fa-scissors}" />