mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-07-11 09:54:02 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b19e2ac65d | |||
| 6953128ff2 | |||
| b72990bf53 | |||
| 732e882524 | |||
| 68432d210b | |||
| e47417a161 | |||
| d665de1f4f | |||
| 680b5b719a | |||
| b236732713 | |||
| 97aed15d19 | |||
| 134773e95f | |||
| 5072904162 | |||
| 6fb71b08ae | |||
| 59a6a3a5f7 | |||
| 8b7d27a071 | |||
| 45c66eaf66 | |||
| 905a41a643 | |||
| 54e9b40cd7 |
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "取消"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "所有支援的格式"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "搜尋"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "從大到小"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "沒有執行任何操作。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
+381
-1856
File diff suppressed because it is too large
Load Diff
@@ -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": "",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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": "反修剪"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
@@ -8,7 +8,7 @@ namespace ARMeilleure.CodeGen
|
||||
/// <summary>
|
||||
/// Represents a compiled function.
|
||||
/// </summary>
|
||||
readonly struct CompiledFunction
|
||||
public readonly struct CompiledFunction
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the machine code of the <see cref="CompiledFunction"/>.
|
||||
@@ -44,10 +44,11 @@ namespace ARMeilleure.CodeGen
|
||||
/// <typeparamref name="T"/> pointing to the mapped function.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of delegate</typeparam>
|
||||
/// <param name="jitCache">The jit cache to map the function into</param>
|
||||
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
|
||||
public T Map<T>()
|
||||
public T Map<T>(JitCache jitCache)
|
||||
{
|
||||
return MapWithPointer<T>(out _);
|
||||
return MapWithPointer<T>(jitCache, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,11 +56,12 @@ namespace ARMeilleure.CodeGen
|
||||
/// <typeparamref name="T"/> pointing to the mapped function.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of delegate</typeparam>
|
||||
/// <param name="jitCache">The jit cache to map the function into</param>
|
||||
/// <param name="codePointer">Pointer to the function code in memory</param>
|
||||
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
|
||||
public T MapWithPointer<T>(out nint codePointer)
|
||||
public T MapWithPointer<T>(JitCache jitCache, out nint codePointer)
|
||||
{
|
||||
codePointer = JitCache.Map(this);
|
||||
codePointer = jitCache.Map(this);
|
||||
|
||||
return Marshal.GetDelegateForFunctionPointer<T>(codePointer);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
|
||||
/// <summary>
|
||||
/// Represents a relocation.
|
||||
/// </summary>
|
||||
readonly struct RelocEntry
|
||||
public readonly struct RelocEntry
|
||||
{
|
||||
public const int Stride = 13; // Bytes.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
|
||||
/// <summary>
|
||||
/// Represents relocation information about a <see cref="CompiledFunction"/>.
|
||||
/// </summary>
|
||||
readonly struct RelocInfo
|
||||
public readonly struct RelocInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="RelocInfo"/>.
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
|
||||
/// <summary>
|
||||
/// Represents a symbol.
|
||||
/// </summary>
|
||||
readonly struct Symbol
|
||||
public readonly struct Symbol
|
||||
{
|
||||
private readonly ulong _value;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
|
||||
/// <summary>
|
||||
/// Types of <see cref="Symbol"/>.
|
||||
/// </summary>
|
||||
enum SymbolType : byte
|
||||
public enum SymbolType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Refers to nothing, i.e no symbol.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ARMeilleure.CodeGen.Unwinding
|
||||
{
|
||||
struct UnwindInfo
|
||||
public struct UnwindInfo
|
||||
{
|
||||
public const int Stride = 4; // Bytes.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ARMeilleure.CodeGen.Unwinding
|
||||
{
|
||||
enum UnwindPseudoOp
|
||||
public enum UnwindPseudoOp
|
||||
{
|
||||
PushReg = 0,
|
||||
SetFrame = 1,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ARMeilleure.CodeGen.Unwinding
|
||||
{
|
||||
struct UnwindPushEntry
|
||||
public struct UnwindPushEntry
|
||||
{
|
||||
public const int Stride = 16; // Bytes.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ARMeilleure.Common
|
||||
public readonly struct AddressTableLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the index of the <see cref="Level"/> in the guest address.
|
||||
/// Gets the index of the <see cref="Index"/> in the guest address.
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
|
||||
@@ -19,46 +19,22 @@ namespace ARMeilleure.Common
|
||||
new( 7, 8),
|
||||
new( 1, 6)
|
||||
];
|
||||
|
||||
private static readonly AddressTableLevel[] _levels64BitSparseTiny =
|
||||
|
||||
private static readonly AddressTableLevel[] _monoSparse64Bit =
|
||||
[
|
||||
new( 11, 28),
|
||||
new( 2, 9)
|
||||
new( 2, 37)
|
||||
];
|
||||
|
||||
private static readonly AddressTableLevel[] _levels32BitSparseTiny =
|
||||
private static readonly AddressTableLevel[] _monoSparse32Bit =
|
||||
[
|
||||
new( 10, 22),
|
||||
new( 1, 9)
|
||||
new( 1, 31)
|
||||
];
|
||||
|
||||
private static readonly AddressTableLevel[] _levels64BitSparseGiant =
|
||||
[
|
||||
new( 38, 1),
|
||||
new( 2, 36)
|
||||
];
|
||||
|
||||
private static readonly AddressTableLevel[] _levels32BitSparseGiant =
|
||||
[
|
||||
new( 31, 1),
|
||||
new( 1, 30)
|
||||
];
|
||||
|
||||
//high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization
|
||||
//low power will never run worse than non-sparse, but for most systems it won't be necessary
|
||||
//high power is always used, but I've left low power in here for future reference
|
||||
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false)
|
||||
|
||||
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse)
|
||||
{
|
||||
if (sparse)
|
||||
{
|
||||
if (lowPower)
|
||||
{
|
||||
return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny;
|
||||
}
|
||||
else
|
||||
{
|
||||
return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant;
|
||||
}
|
||||
return for64Bits ? _monoSparse64Bit : _monoSparse32Bit;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using ARMeilleure.IntermediateRepresentation;
|
||||
using ARMeilleure.State;
|
||||
using ARMeilleure.Translation;
|
||||
using ARMeilleure.Translation.PTC;
|
||||
|
||||
using System.Linq;
|
||||
using static ARMeilleure.Instructions.InstEmitHelper;
|
||||
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace ARMeilleure.Instructions
|
||||
}
|
||||
else if (table.Sparse)
|
||||
{
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
|
||||
// Deliberately attempts to avoid branches.
|
||||
|
||||
Operand tableBase = !context.HasPtc ?
|
||||
@@ -247,18 +247,15 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
hostAddress = tableBase;
|
||||
|
||||
for (int i = 0; i < table.Levels.Length; i++)
|
||||
{
|
||||
AddressTableLevel level = table.Levels[i];
|
||||
int clearBits = 64 - (level.Index + level.Length);
|
||||
AddressTableLevel level = table.Levels.Last();
|
||||
int clearBits = 64 - (level.Index + level.Length);
|
||||
|
||||
Operand index = context.ShiftLeft(
|
||||
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
|
||||
Const(3)
|
||||
);
|
||||
Operand index = context.ShiftLeft(
|
||||
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
|
||||
Const(3)
|
||||
);
|
||||
|
||||
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
|
||||
}
|
||||
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<DebugPartialUnmap>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugPartialUnmap>(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<DebugThreadLocalMapGetOrReserve>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugThreadLocalMapGetOrReserve>(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<DebugNativeWriteLoop>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugNativeWriteLoop>(jitCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ARMeilleure.Translation.Cache
|
||||
{
|
||||
readonly struct CacheEntry : IComparable<CacheEntry>
|
||||
public readonly struct CacheEntry : IComparable<CacheEntry>
|
||||
{
|
||||
public int Offset { get; }
|
||||
public int Size { get; }
|
||||
|
||||
@@ -22,12 +22,31 @@ namespace ARMeilleure.Translation.Cache
|
||||
return Offset.CompareTo(other.Offset);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly int _regionSize;
|
||||
private int _regionCount;
|
||||
|
||||
private readonly List<MemoryBlock> _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];
|
||||
|
||||
|
||||
@@ -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<CacheMemoryAllocator> _cacheAllocators = [];
|
||||
private readonly CacheMemoryAllocator _cacheAllocator;
|
||||
|
||||
private static readonly List<CacheEntry> _cacheEntries = [];
|
||||
private readonly List<CacheEntry> _cacheEntries = [];
|
||||
|
||||
private static readonly Lock _lock = new();
|
||||
private static bool _initialized;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
private static readonly List<ReservedRegion> _jitRegions = [];
|
||||
private static int _activeRegionIndex = 0;
|
||||
private readonly List<ReservedRegion> _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,67 @@ 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;
|
||||
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
|
||||
int regionEnd = endOffs % (int)CacheSize == 0
|
||||
? (((int)CacheSize) + _pageMask) & ~_pageMask
|
||||
: ((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 == 0
|
||||
? (((int)CacheSize) + _pageMask) & ~_pageMask
|
||||
: ((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 +193,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 +207,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 +230,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ulong, InternalFunctionHandler> _functionTableHandlers = new();
|
||||
|
||||
private unsafe static RuntimeFunction* _runtimeFunction;
|
||||
static JitUnwindWindows()
|
||||
{
|
||||
_sizeOfRuntimeFunction = Marshal.SizeOf<RuntimeFunction>();
|
||||
}
|
||||
|
||||
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<RuntimeFunction>();
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 = 7018; //! 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<PtcLoadingState, int, int> 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<uint> callCounter,
|
||||
ulong guestSize,
|
||||
@@ -790,7 +795,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
bool highCq)
|
||||
{
|
||||
CompiledFunction cFunc = new(code, unwindInfo, RelocInfo.Empty);
|
||||
GuestFunction gFunc = cFunc.MapWithPointer<GuestFunction>(out nint gFuncPointer);
|
||||
GuestFunction gFunc = cFunc.MapWithPointer<GuestFunction>(_jitCache, out nint gFuncPointer);
|
||||
|
||||
return new TranslatedFunction(gFunc, gFuncPointer, callCounter, guestSize, highCq);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace ARMeilleure.Translation
|
||||
private readonly IJitMemoryAllocator _allocator;
|
||||
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
|
||||
|
||||
public readonly JitCache JitCache;
|
||||
private readonly Ptc _ptc;
|
||||
|
||||
internal TranslatorCache<TranslatedFunction> Functions { get; }
|
||||
@@ -43,16 +44,17 @@ namespace ARMeilleure.Translation
|
||||
|
||||
_oldFuncs = new ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>>();
|
||||
|
||||
_ptc = new Ptc();
|
||||
JitCache = new JitCache(allocator);
|
||||
|
||||
_ptc = new Ptc(JitCache);
|
||||
|
||||
Queue = new TranslatorQueue();
|
||||
|
||||
JitCache.Initialize(allocator);
|
||||
|
||||
CountTable = new EntryTable<uint>();
|
||||
Functions = new TranslatorCache<TranslatedFunction>();
|
||||
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<GuestFunction>(out nint funcPointer);
|
||||
GuestFunction func = compiledFunc.MapWithPointer<GuestFunction>(JitCache, out nint funcPointer);
|
||||
|
||||
Allocators.ResetAll();
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace ARMeilleure.Translation
|
||||
/// </summary>
|
||||
class TranslatorStubs : IDisposable
|
||||
{
|
||||
private readonly JitCache _jitCache;
|
||||
|
||||
private readonly Lazy<nint> _slowDispatchStub;
|
||||
|
||||
private bool _disposed;
|
||||
@@ -83,11 +85,14 @@ namespace ARMeilleure.Translation
|
||||
/// Initializes a new instance of the <see cref="TranslatorStubs"/> class with the specified
|
||||
/// <see cref="Translator"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="jitCache">Jit cache to map functions in</param>
|
||||
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
|
||||
public TranslatorStubs(IAddressTable<ulong> functionTable)
|
||||
public TranslatorStubs(JitCache jitCache, IAddressTable<ulong> 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;
|
||||
@@ -156,7 +161,7 @@ namespace ARMeilleure.Translation
|
||||
context.BranchIfTrue(lblFallback, masked);
|
||||
|
||||
Operand index = default;
|
||||
Operand page = Const((long)_functionTable.Base);
|
||||
Operand page = Const(_functionTable.Base);
|
||||
|
||||
for (int i = 0; i < _functionTable.Levels.Length; i++)
|
||||
{
|
||||
@@ -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>();
|
||||
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>(_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>();
|
||||
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>(_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<DispatcherFunction>();
|
||||
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DispatcherFunction>(_jitCache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<WrapperFunction>();
|
||||
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<WrapperFunction>(_jitCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FpFlagsPInvokeTest>();
|
||||
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<FpFlagsPInvokeTest>(jitCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Ryujinx.BuildValidationTasks
|
||||
|
||||
using (StreamWriter sw = new(path))
|
||||
{
|
||||
sw.Write(jsonString);
|
||||
sw.Write($"{jsonString}\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<AssignedInputDeviceType>))]
|
||||
public enum AssignedInputDeviceType
|
||||
{
|
||||
Keyboard,
|
||||
Controller,
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,12 @@ namespace Ryujinx.Common.Configuration.Hid
|
||||
/// </summary>
|
||||
public PlayerIndex PlayerIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow a keyboard configuration to temporarily promote to a connected gamepad,
|
||||
/// while preserving the existing keyboard fallback path when that gamepad disappears.
|
||||
/// </summary>
|
||||
public bool EnableDynamicGamepadSwap { get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
|
||||
@@ -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<AssignedInputDevice> Devices { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -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<AssignedInputDevice> 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<AssignedInputDevice> Deduplicate(IEnumerable<AssignedInputDevice> devices)
|
||||
{
|
||||
List<AssignedInputDevice> 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<AssignedInputDevice> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -81,12 +81,13 @@ namespace ARMeilleure.Common
|
||||
|
||||
private bool _disposed;
|
||||
private TEntry** _table;
|
||||
private TEntry* _sparseTable;
|
||||
private readonly List<AddressTablePage> _pages;
|
||||
private TEntry _fill;
|
||||
|
||||
private readonly MemoryBlock _sparseFill;
|
||||
private readonly SparseMemoryBlock _fillBottomLevel;
|
||||
private readonly TEntry* _fillBottomLevelPtr;
|
||||
private MemoryBlock _sparseFill;
|
||||
private SparseMemoryBlock _fillBottomLevel;
|
||||
private TEntry* _fillBottomLevelPtr;
|
||||
|
||||
private readonly List<TableSparseBlock> _sparseReserved;
|
||||
private readonly ReaderWriterLockSlim _sparseLock;
|
||||
@@ -122,16 +123,23 @@ namespace ARMeilleure.Common
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
lock (_pages)
|
||||
if (Sparse)
|
||||
{
|
||||
return (nint)GetRootPage();
|
||||
return (nint)_sparseTable;
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_pages)
|
||||
{
|
||||
return (nint)GetRootPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
|
||||
/// <see cref="Level"/>.
|
||||
/// <see cref="AddressTableLevel"/>.
|
||||
/// </summary>
|
||||
/// <param name="levels">Levels for the address table</param>
|
||||
/// <param name="sparse">True if the bottom page should be sparsely mapped</param>
|
||||
@@ -156,18 +164,8 @@ namespace ARMeilleure.Common
|
||||
if (sparse)
|
||||
{
|
||||
// If the address table is sparse, allocate a fill block
|
||||
|
||||
_sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul
|
||||
|
||||
ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry);
|
||||
|
||||
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
|
||||
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
|
||||
|
||||
_sparseReserved = [];
|
||||
_sparseLock = new ReaderWriterLockSlim();
|
||||
|
||||
_sparseBlockSize = bottomLevelSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,13 +207,24 @@ namespace ARMeilleure.Common
|
||||
public void SignalCodeRange(ulong address, ulong size)
|
||||
{
|
||||
AddressTableLevel bottom = Levels.Last();
|
||||
ulong bottomLevelEntries = 1ul << bottom.Length;
|
||||
|
||||
ulong entryIndex = address >> bottom.Index;
|
||||
|
||||
ulong entries = size >> bottom.Index;
|
||||
entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries);
|
||||
|
||||
if (Sparse)
|
||||
{
|
||||
ulong bottomLevelSize = (ulong)BitUtils.Pow2RoundUp((int)entries) * (ulong)sizeof(TEntry);
|
||||
|
||||
_sparseFill = new MemoryBlock(bottomLevelSize, MemoryAllocationFlags.Mirrorable);
|
||||
|
||||
_sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry));
|
||||
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
|
||||
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
|
||||
|
||||
_sparseBlockSize = bottomLevelSize;
|
||||
|
||||
_sparseTable = (TEntry*)Allocate((int)entries, Fill, leaf: true);
|
||||
|
||||
_sparseTable -= Levels.Last().GetValue(address);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -234,15 +243,26 @@ namespace ARMeilleure.Common
|
||||
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
|
||||
}
|
||||
|
||||
lock (_pages)
|
||||
if (Sparse)
|
||||
{
|
||||
TEntry* page = GetPage(address);
|
||||
long index = Levels.Last().GetValue(address);
|
||||
|
||||
EnsureMapped((nint)(_sparseTable + index));
|
||||
|
||||
return ref _sparseTable[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_pages)
|
||||
{
|
||||
TEntry* page = GetPage(address);
|
||||
|
||||
long index = Levels[^1].GetValue(address);
|
||||
long index = Levels.Last().GetValue(address);
|
||||
|
||||
EnsureMapped((nint)(page + index));
|
||||
EnsureMapped((nint)(page + index));
|
||||
|
||||
return ref page[index];
|
||||
return ref page[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Ryujinx.Cpu.LightningJit.CodeGen;
|
||||
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
@@ -189,7 +190,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
|
||||
}
|
||||
else if (inlineLookup)
|
||||
{
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
|
||||
|
||||
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
|
||||
|
||||
@@ -203,18 +204,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
|
||||
// Index into the table.
|
||||
asm.Mov(rn, tableBase);
|
||||
|
||||
for (int i = 0; i < funcTable.Levels.Length; i++)
|
||||
{
|
||||
AddressTableLevel level = funcTable.Levels[i];
|
||||
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
|
||||
asm.Lsl(indexReg, indexReg, Const(3));
|
||||
AddressTableLevel level = funcTable.Levels.Last();
|
||||
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
|
||||
asm.Lsl(indexReg, indexReg, Const(3));
|
||||
|
||||
// Index into the page.
|
||||
asm.Add(rn, rn, indexReg);
|
||||
// Index into the page.
|
||||
asm.Add(rn, rn, indexReg);
|
||||
|
||||
// Load the page address.
|
||||
asm.LdrRiUn(rn, rn, 0);
|
||||
}
|
||||
// Load the page address.
|
||||
asm.LdrRiUn(rn, rn, 0);
|
||||
|
||||
if (tempGuestAddress != -1)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Ryujinx.Cpu.LightningJit.CodeGen;
|
||||
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -355,7 +356,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
|
||||
}
|
||||
else if (inlineLookup)
|
||||
{
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
|
||||
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
|
||||
|
||||
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
|
||||
|
||||
@@ -369,18 +370,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
|
||||
// Index into the table.
|
||||
asm.Mov(rn, tableBase);
|
||||
|
||||
for (int i = 0; i < funcTable.Levels.Length; i++)
|
||||
{
|
||||
AddressTableLevel level = funcTable.Levels[i];
|
||||
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
|
||||
asm.Lsl(indexReg, indexReg, Const(3));
|
||||
AddressTableLevel level = funcTable.Levels.Last();
|
||||
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
|
||||
asm.Lsl(indexReg, indexReg, Const(3));
|
||||
|
||||
// Index into the page.
|
||||
asm.Add(rn, rn, indexReg);
|
||||
// Index into the page.
|
||||
asm.Add(rn, rn, indexReg);
|
||||
|
||||
// Load the page address.
|
||||
asm.LdrRiUn(rn, rn, 0);
|
||||
}
|
||||
// Load the page address.
|
||||
asm.LdrRiUn(rn, rn, 0);
|
||||
|
||||
if (tempGuestAddress != -1)
|
||||
{
|
||||
|
||||
@@ -23,12 +23,31 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
return Offset.CompareTo(other.Offset);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly int _regionSize;
|
||||
private int _regionCount;
|
||||
|
||||
private readonly List<MemoryBlock> _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];
|
||||
|
||||
|
||||
@@ -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<CacheEntry> _cacheEntries = [];
|
||||
private readonly List<CacheEntry> _cacheEntries = [];
|
||||
|
||||
private static readonly Lock _lock = new();
|
||||
private static bool _initialized;
|
||||
private static readonly List<ReservedRegion> _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<ReservedRegion> _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<byte> code)
|
||||
public nint Map(ReadOnlySpan<byte> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Ryujinx.Cpu.LightningJit
|
||||
private static bool IsNoWxPlatform => false;
|
||||
|
||||
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _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<TranslatedFunction>();
|
||||
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<ulong, TranslatedFunction> 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();
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Ryujinx.Cpu.LightningJit
|
||||
{
|
||||
private delegate ulong GetFunctionAddressDelegate(nint framePointer, ulong address);
|
||||
|
||||
private readonly JitCache _jitCache;
|
||||
|
||||
private readonly Lazy<nint> _slowDispatchStub;
|
||||
|
||||
private bool _disposed;
|
||||
@@ -76,13 +78,16 @@ namespace Ryujinx.Cpu.LightningJit
|
||||
/// Initializes a new instance of the <see cref="TranslatorStubs"/> class with the specified
|
||||
/// <see cref="Translator"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="jitCache">Jit cache to map functions in</param>
|
||||
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
|
||||
/// <param name="noWxCache">Cache used on platforms that enforce W^X, otherwise should be null</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
|
||||
public TranslatorStubs(IAddressTable<ulong> functionTable, NoWxCache noWxCache)
|
||||
public TranslatorStubs(JitCache jitCache, IAddressTable<ulong> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1088,6 +1088,12 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types
|
||||
|
||||
public void GetMvs(Span<Vp9MvRef> mvs)
|
||||
{
|
||||
if (FrameIsIntraOnly())
|
||||
{
|
||||
mvs.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mvs.Length > CurFrameMvs.Length)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() ?? "<missing>"} 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() ?? "<missing>"} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
|
||||
int serialNumber = 0;
|
||||
string? serial = SDL_GetGamepadSerial(gamepadHandle);
|
||||
string serial = SDL_GetGamepadSerial(gamepadHandle);
|
||||
if (serial is not null)
|
||||
{
|
||||
int.TryParse(serial, out serialNumber);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<IGamepad> _assignedControllerGamepads = [];
|
||||
private readonly List<StandardControllerInputConfig> _assignedControllerConfigs = [];
|
||||
private InputConfig _config;
|
||||
private InputConfig _activeConfig;
|
||||
private StandardKeyboardInputConfig _keyboardConfig;
|
||||
private StandardControllerInputConfig _controllerConfig;
|
||||
private GamepadStateSnapshot _previousKeyboardState;
|
||||
private readonly List<GamepadStateSnapshot> _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<AssignedInputDevice> ResolveDynamicControllerAssignments(IGamepadDriver gamepadDriver, InputConfig config)
|
||||
{
|
||||
if (gamepadDriver == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
List<AssignedInputDevice> 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.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.AmplitudeHigh}), " +
|
||||
$"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
|
||||
$"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.FrequencyHigh}), " +
|
||||
$"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.AmplitudeLow}), " +
|
||||
$"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
|
||||
$"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({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<StandardKeyboardInputConfig>(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<StandardControllerInputConfig>(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>(T config) where T : InputConfig
|
||||
{
|
||||
return JsonHelper.Deserialize(
|
||||
JsonHelper.Serialize(config, _serializerContext.InputConfig),
|
||||
_serializerContext.InputConfig) as T;
|
||||
}
|
||||
|
||||
private static bool TryLoadAssignedProfile<T>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Ryujinx.Input.HLE
|
||||
|
||||
private List<InputConfig> _inputConfig;
|
||||
private List<InputConfig> _requestedInputConfig;
|
||||
private List<PlayerInputAssignment> _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<InputConfig> requestedInputConfig;
|
||||
List<PlayerInputAssignment> 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<InputConfig> requestedInputConfig;
|
||||
List<PlayerInputAssignment> 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> inputConfig, bool enableKeyboard, bool enableMouse)
|
||||
{
|
||||
ReloadConfiguration(inputConfig, [], enableKeyboard, enableMouse);
|
||||
}
|
||||
|
||||
public void ReloadConfiguration(List<InputConfig> inputConfig, List<PlayerInputAssignment> 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> inputConfig, bool enableKeyboard, bool enableMouse)
|
||||
{
|
||||
Initialize(device, inputConfig, [], enableKeyboard, enableMouse);
|
||||
}
|
||||
|
||||
public void Initialize(Switch device, List<InputConfig> inputConfig, List<PlayerInputAssignment> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<int>.MapSize; i++)
|
||||
{
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SplashLocales>(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<SplashLocales>(data);
|
||||
return s_splashJson.Locales[ConfigurationState.Instance.UI.LanguageCode.Value].GetRandomElement();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return _finalSplash;
|
||||
}
|
||||
|
||||
private struct SplashLocales
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -426,6 +426,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public List<InputConfig> InputConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Player-level input routing assignments
|
||||
/// </summary>
|
||||
public List<PlayerInputAssignment> PlayerInputAssignments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow mapping the same input device to multiple players.
|
||||
/// </summary>
|
||||
public bool AllowDuplicateDeviceAssignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The speed of spectrum cycling for the Rainbow LED feature.
|
||||
/// </summary>
|
||||
@@ -492,15 +502,20 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
public bool DebuggerSuspendOnStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show toggles for dirty hacks in the UI.
|
||||
/// Show toggles for dirty hacks in the UI.
|
||||
/// </summary>
|
||||
public bool ShowDirtyHacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The packed values of the enabled dirty hacks.
|
||||
/// The packed values of the enabled dirty hacks.
|
||||
/// </summary>
|
||||
public ulong[] DirtyHacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pause emulation while scanning Amiibo
|
||||
/// </summary>
|
||||
public bool PauseEmulationWhileScanningAmiibo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration file from disk
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -191,6 +191,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IsAscendingOrder { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show Dynamic Input Swap first-use warning
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowDynamicInputSwapWarning { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pause emulation while scanning Amiibo
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> PauseEmulationWhileScanningAmiibo { get; private set; }
|
||||
|
||||
public UISection()
|
||||
{
|
||||
GuiColumns = new Columns();
|
||||
@@ -210,6 +220,10 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
LanguageCode = new ReactiveObject<string>();
|
||||
ShowConsole = new ReactiveObject<bool>();
|
||||
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
|
||||
ShowDynamicInputSwapWarning = new ReactiveObject<bool>();
|
||||
ShowDynamicInputSwapWarning.Value = true;
|
||||
PauseEmulationWhileScanningAmiibo = new ReactiveObject<bool>();
|
||||
PauseEmulationWhileScanningAmiibo.Value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +527,19 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public ReactiveObject<List<InputConfig>> InputConfig { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ReactiveObject<List<PlayerInputAssignment>> PlayerInputAssignments { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow mapping the same input device to multiple players.
|
||||
/// This is a global setting shared across all players.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> AllowDuplicateDeviceAssignment { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The speed of spectrum cycling for the Rainbow LED feature.
|
||||
/// </summary>
|
||||
@@ -525,6 +552,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
DisableInputWhenOutOfFocus = new ReactiveObject<bool>();
|
||||
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
|
||||
InputConfig = new ReactiveObject<List<InputConfig>>();
|
||||
PlayerInputAssignments = new ReactiveObject<List<PlayerInputAssignment>>();
|
||||
AllowDuplicateDeviceAssignment = new ReactiveObject<bool>();
|
||||
RainbowSpeed = new ReactiveObject<float>();
|
||||
RainbowSpeed.Event += (_, args) => Rainbow.Speed = args.NewValue;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, string> outDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(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<string, string> rpcdict = new() { { "Details", line1 }, {"State", line2} };
|
||||
return JsonSerializer.Serialize(rpcdict);
|
||||
}
|
||||
|
||||
private static string TruncateToByteLength(string input)
|
||||
{
|
||||
if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit)
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
<MenuItem
|
||||
Command="{Binding TrimXci}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||
Header="{ext:Locale GameListContextMenu_TrimXCIButton}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
Icon="{ext:Icon fa-solid fa-scissors}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
IsVisible="{Binding IsXCIFile}"
|
||||
Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon fa-solid fa-memory}">
|
||||
<MenuItem
|
||||
Command="{Binding PurgePtcCache}"
|
||||
|
||||
@@ -18,6 +18,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class CheckBoxDialogResult
|
||||
{
|
||||
public bool IsChecked { get; set; }
|
||||
}
|
||||
|
||||
public static class ContentDialogHelper
|
||||
{
|
||||
private static bool _isChoiceDialogOpen;
|
||||
@@ -431,6 +436,67 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
return response == UserResult.Yes;
|
||||
}
|
||||
|
||||
internal static async Task<CheckBoxDialogResult> CreateCheckBoxDialog(string title, string primaryText, string checkBoxText, bool isCheckedDefault)
|
||||
{
|
||||
CheckBoxDialogResult result = new CheckBoxDialogResult { IsChecked = isCheckedDefault };
|
||||
|
||||
Grid content = new()
|
||||
{
|
||||
RowDefinitions = [new(), new(), new()],
|
||||
ColumnDefinitions = [new(GridLength.Auto), new()],
|
||||
MinHeight = 80,
|
||||
};
|
||||
|
||||
content.Children.Add(new SymbolIcon
|
||||
{
|
||||
Symbol = (Symbol)Symbol.Important,
|
||||
Margin = new Thickness(10),
|
||||
FontSize = 40,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
GridColumn = 0,
|
||||
GridRow = 0,
|
||||
GridRowSpan = 2
|
||||
});
|
||||
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = primaryText,
|
||||
Margin = new Thickness(5),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 450,
|
||||
GridColumn = 1,
|
||||
GridRow = 0
|
||||
});
|
||||
|
||||
CheckBox checkBox = new()
|
||||
{
|
||||
Content = checkBoxText,
|
||||
IsChecked = isCheckedDefault,
|
||||
Margin = new Thickness(5),
|
||||
GridColumn = 1,
|
||||
GridRow = 1
|
||||
};
|
||||
|
||||
checkBox.IsCheckedChanged += (s, e) =>
|
||||
{
|
||||
result.IsChecked = checkBox.IsChecked == true;
|
||||
};
|
||||
|
||||
content.Children.Add(checkBox);
|
||||
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = title,
|
||||
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
Content = content,
|
||||
};
|
||||
|
||||
await ShowAsync(contentDialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static async Task<UserResult> CreateUpdaterChoiceDialog(string title, string primary, string secondaryText, string changelogUrl)
|
||||
{
|
||||
if (_isChoiceDialogOpen)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Ava.UI.ViewModels.Input;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class ProfileNameLinkedConverter : IValueConverter
|
||||
{
|
||||
public static readonly ProfileNameLinkedConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string profileName || string.IsNullOrWhiteSpace(profileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameter is InputViewModel viewModel)
|
||||
{
|
||||
return viewModel.IsProfileNameLinked(profileName);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,32 +12,57 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
internal class XCITrimmerFileSpaceSavingsConverter : IValueConverter
|
||||
{
|
||||
private const long _bytesPerMB = 1024 * 1024;
|
||||
|
||||
public static readonly XCITrimmerFileSpaceSavingsConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is UnsetValueType)
|
||||
{
|
||||
if (value == null || value == AvaloniaProperty.UnsetValue)
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
|
||||
if (!targetType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value is not XCITrimmerFileModel app)
|
||||
{
|
||||
return null;
|
||||
|
||||
long originalSize = app.OriginalSizeB;
|
||||
long currentSavings = app.CurrentSavingsB;
|
||||
long potentialSavings = app.PotentialSavingsB;
|
||||
|
||||
if (originalSize <= 0)
|
||||
{
|
||||
return GetFormattedString(app, 0, 0);
|
||||
}
|
||||
|
||||
long mbValue = 0;
|
||||
double percentage = 0;
|
||||
|
||||
if (currentSavings > 0)
|
||||
{
|
||||
mbValue = (currentSavings / _bytesPerMB).CoerceAtLeast(0);
|
||||
percentage = (currentSavings / (double)originalSize) * 100;
|
||||
}
|
||||
else if (potentialSavings > 0)
|
||||
{
|
||||
mbValue = (potentialSavings / _bytesPerMB).CoerceAtLeast(0);
|
||||
percentage = (potentialSavings / (double)originalSize) * 100;
|
||||
}
|
||||
|
||||
return GetFormattedString(app, mbValue, percentage);
|
||||
}
|
||||
|
||||
private string GetFormattedString(XCITrimmerFileModel app, long mb, double percentage)
|
||||
{
|
||||
// Round percentage to 1 decimal place
|
||||
double roundedPercentage = Math.Round(percentage, 1);
|
||||
|
||||
if (app.CurrentSavingsB < app.PotentialSavingsB)
|
||||
{
|
||||
return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, ((app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB).CoerceAtLeast(0));
|
||||
return LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.XCITrimmer_CalculatedSavingsLabel, mb, roundedPercentage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, (app.CurrentSavingsB / _bytesPerMB).CoerceAtLeast(0));
|
||||
return LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.XCITrimmer_CalculatedSavingsLabel, mb, roundedPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
@@ -16,26 +16,30 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is UnsetValueType)
|
||||
{
|
||||
return BindingOperations.DoNothing;
|
||||
}
|
||||
|
||||
if (!targetType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value is not XCITrimmerFileModel app)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return default(Symbol);
|
||||
|
||||
bool isProcessing = app.PercentageProgress != null;
|
||||
|
||||
if (isProcessing)
|
||||
return Symbol.Sync;
|
||||
|
||||
return app.PercentageProgress != null ? String.Empty :
|
||||
app.ProcessingOutcome is not OperationOutcome.Successful and not OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] :
|
||||
app.Trimmable & app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] :
|
||||
app.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] :
|
||||
app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] :
|
||||
String.Empty;
|
||||
if (app.ProcessingOutcome is not OperationOutcome.Successful
|
||||
and not OperationOutcome.Undetermined)
|
||||
return Symbol.ImportantFilled;
|
||||
|
||||
if (app.Trimmable && app.Untrimmable)
|
||||
return Symbol.Repair;
|
||||
|
||||
if (app.Trimmable)
|
||||
return Symbol.Clear;
|
||||
|
||||
if (app.Untrimmable)
|
||||
return Symbol.Checkmark;
|
||||
|
||||
return Symbol.Help;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
try
|
||||
{
|
||||
string labelsJson = File.ReadAllText(labelsPath);
|
||||
Dictionary<string, string>? labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
|
||||
Dictionary<string, string> labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
|
||||
|
||||
if (labels != null)
|
||||
{
|
||||
|
||||
@@ -9,17 +9,15 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public string LocalizedText => opOutcome switch
|
||||
{
|
||||
OperationOutcome.NoTrimNecessary => LocaleManager.Instance[LocaleKeys.TrimXCIFileNoTrimNecessary],
|
||||
OperationOutcome.NoUntrimPossible => LocaleManager.Instance[LocaleKeys.TrimXCIFileNoUntrimPossible],
|
||||
OperationOutcome.ReadOnlyFileCannotFix => LocaleManager.Instance[
|
||||
LocaleKeys.TrimXCIFileReadOnlyFileCannotFix],
|
||||
OperationOutcome.FreeSpaceCheckFailed => LocaleManager.Instance[
|
||||
LocaleKeys.TrimXCIFileFreeSpaceCheckFailed],
|
||||
OperationOutcome.InvalidXCIFile => LocaleManager.Instance[LocaleKeys.TrimXCIFileInvalidXCIFile],
|
||||
OperationOutcome.FileIOWriteError => LocaleManager.Instance[LocaleKeys.TrimXCIFileFileIOWriteError],
|
||||
OperationOutcome.FileSizeChanged => LocaleManager.Instance[LocaleKeys.TrimXCIFileFileSizeChanged],
|
||||
OperationOutcome.Cancelled => LocaleManager.Instance[LocaleKeys.TrimXCIFileCancelled],
|
||||
OperationOutcome.Undetermined => LocaleManager.Instance[LocaleKeys.TrimXCIFileFileUndertermined],
|
||||
OperationOutcome.NoTrimNecessary => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_NoTrimNecessaryMessage],
|
||||
OperationOutcome.NoUntrimPossible => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_NoUntrimPossibleMessage],
|
||||
OperationOutcome.ReadOnlyFileCannotFix => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_ReadOnlyFileCannotFixMessage],
|
||||
OperationOutcome.FreeSpaceCheckFailed => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_FreeSpaceCheckFailedMessage],
|
||||
OperationOutcome.InvalidXCIFile => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_InvalidDataMessage],
|
||||
OperationOutcome.FileIOWriteError => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_WriteErrorMessage],
|
||||
OperationOutcome.FileSizeChanged => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_SizeChangedMessage],
|
||||
OperationOutcome.Cancelled => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_TrimCancelledMessage],
|
||||
OperationOutcome.Undetermined => LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_NoOperationPerformedMessage],
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Input
|
||||
{
|
||||
public class PlayerInputDeviceAssignmentItem : BaseModel
|
||||
{
|
||||
public DeviceType DeviceType { get; init; }
|
||||
|
||||
public string Id { get; init; }
|
||||
|
||||
public string Name { get; init; }
|
||||
|
||||
public AssignedInputDeviceType AssignedType =>
|
||||
DeviceType == DeviceType.Keyboard ? AssignedInputDeviceType.Keyboard : AssignedInputDeviceType.Controller;
|
||||
|
||||
public bool IsAssigned
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasBoundProfileName => !string.IsNullOrWhiteSpace(BoundProfileName);
|
||||
|
||||
public string BoundProfileName
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasBoundProfileName));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasAssignedToPlayers => !string.IsNullOrWhiteSpace(AssignedToPlayers);
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of player names (e.g. "Player 1, Player 3")
|
||||
/// that have this device assigned. Empty if no other player uses it.
|
||||
/// </summary>
|
||||
public string AssignedToPlayers
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasAssignedToPlayers));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when this device is assigned to another player and
|
||||
/// AllowDuplicateDeviceAssignment is disabled, making it unclickable.
|
||||
/// </summary>
|
||||
public bool IsDisabledByOtherPlayer
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ using Avalonia.Collections;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Path = System.IO.Path;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Common.Models.Amiibo;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
@@ -20,31 +22,39 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public partial class AmiiboWindowViewModel : BaseModel, IDisposable
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static bool _cachedUseRandomUuid;
|
||||
|
||||
private const string DefaultJson = "{ \"amiibo\": [] }";
|
||||
private const float AmiiboImageSize = 350f;
|
||||
|
||||
private readonly string _amiiboJsonPath;
|
||||
private readonly byte[] _amiiboLogoBytes;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly AmiiboWindow _owner;
|
||||
|
||||
private List<AmiiboApi> _amiiboList;
|
||||
private AvaloniaList<AmiiboApi> _amiibos;
|
||||
private ObservableCollection<string> _amiiboSeries;
|
||||
public enum AmiiboSortField
|
||||
{
|
||||
Name,
|
||||
|
||||
}
|
||||
|
||||
private int _amiiboSelectedIndex;
|
||||
private int _seriesSelectedIndex;
|
||||
private bool _showAllAmiibo;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static bool _cachedUseRandomUuid;
|
||||
public bool IsSortedByName => _sortField == AmiiboSortField.Name;
|
||||
private const string DefaultJson = "{ \"amiibo\": [] }";
|
||||
private const float AmiiboImageSize = 350f;
|
||||
public string TitleId { get; set; }
|
||||
public string LastScannedAmiiboId { get; set; }
|
||||
public string SortingFieldName => LocaleManager.Instance[LocaleKeys.Common_Sort_NameLabel];
|
||||
private readonly string _amiiboJsonPath;
|
||||
private readonly byte[] _amiiboLogoBytes;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly AmiiboWindow _owner;
|
||||
private List<AmiiboApi> _amiiboList;
|
||||
private AvaloniaList<AmiiboApi> _amiibos;
|
||||
private ObservableCollection<string> _amiiboSeries;
|
||||
private CancellationTokenSource _imageCts = new();
|
||||
private static readonly AmiiboJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public AmiiboWindowViewModel(AmiiboWindow owner, string lastScannedAmiiboId, string titleId)
|
||||
@@ -73,9 +83,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public AmiiboWindowViewModel() { }
|
||||
|
||||
public string TitleId { get; set; }
|
||||
public string LastScannedAmiiboId { get; set; }
|
||||
|
||||
public UserResult Response { get; private set; }
|
||||
|
||||
public bool UseRandomUuid
|
||||
@@ -151,6 +158,42 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool PauseEmulationWhileScanningAmiibo
|
||||
{
|
||||
get => ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value;
|
||||
set
|
||||
{
|
||||
ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value = value;
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
private bool _sortingAscending = true;
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
FilterAmiibo();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SortingAscending
|
||||
{
|
||||
get => _sortingAscending;
|
||||
set
|
||||
{
|
||||
_sortingAscending = value;
|
||||
FilterAmiibo();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Bitmap AmiiboImage { get; set; }
|
||||
|
||||
@@ -166,14 +209,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
_owner.ScannedAmiibo = AmiiboList[AmiiboSelectedIndex];
|
||||
_owner.IsScanned = true;
|
||||
_owner.Close();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_owner.IsScanned = false;
|
||||
_owner.Close();
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -315,42 +358,46 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private void ParseAmiiboData()
|
||||
{
|
||||
_amiiboSeries.Clear();
|
||||
_amiibos.Clear();
|
||||
|
||||
for (int i = 0; i < _amiiboList.Count; i++)
|
||||
foreach (AmiiboApi amiibo in _amiiboList)
|
||||
{
|
||||
if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
|
||||
if (!_amiiboSeries.Contains(amiibo.AmiiboSeries))
|
||||
{
|
||||
if (!ShowAllAmiibo)
|
||||
if (_showAllAmiibo)
|
||||
{
|
||||
foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
if (game.GameId.Contains(TitleId))
|
||||
{
|
||||
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_amiiboSeries.Add(amiibo.AmiiboSeries);
|
||||
}
|
||||
else
|
||||
{
|
||||
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||
bool hasCompatible = amiibo.GamesSwitch.Any(game =>
|
||||
game != null && game.GameId.Contains(TitleId));
|
||||
|
||||
if (hasCompatible)
|
||||
{
|
||||
_amiiboSeries.Add(amiibo.AmiiboSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LastScannedAmiiboId != string.Empty)
|
||||
if (_showAllAmiibo)
|
||||
{
|
||||
SeriesSelectedIndex = -1;
|
||||
}
|
||||
else if (LastScannedAmiiboId != string.Empty)
|
||||
{
|
||||
SelectLastScannedAmiibo();
|
||||
}
|
||||
else
|
||||
else if (_amiiboSeries.Count > 0)
|
||||
{
|
||||
SeriesSelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
SeriesSelectedIndex = -1;
|
||||
}
|
||||
|
||||
FilterAmiibo();
|
||||
}
|
||||
|
||||
private void SelectLastScannedAmiibo()
|
||||
@@ -360,104 +407,109 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
|
||||
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
|
||||
}
|
||||
|
||||
|
||||
private void FilterAmiibo()
|
||||
{
|
||||
_amiibos.Clear();
|
||||
|
||||
List<AmiiboApi> amiiboSortedList;
|
||||
IEnumerable<AmiiboApi> query = _amiiboList.AsEnumerable();
|
||||
|
||||
// If no series selected, just display all available amiibos
|
||||
if (_seriesSelectedIndex < 0)
|
||||
if (_seriesSelectedIndex >= 0 && _seriesSelectedIndex < _amiiboSeries.Count)
|
||||
{
|
||||
amiiboSortedList = _amiiboList
|
||||
.OrderBy(amiibo => amiibo.AmiiboSeries)
|
||||
.ThenBy(x => x.Name)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
amiiboSortedList = _amiiboList
|
||||
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
|
||||
.OrderBy(amiibo => amiibo.Name).ToList();
|
||||
string selectedSeries = _amiiboSeries[_seriesSelectedIndex];
|
||||
query = query.Where(amiibo => amiibo.AmiiboSeries == selectedSeries);
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (int i = 0; i < amiiboSortedList.Count; i++)
|
||||
if (!string.IsNullOrWhiteSpace(_searchText))
|
||||
{
|
||||
if (!_amiibos.Contains(amiiboSortedList[i]))
|
||||
query = query.Where(amiibo =>
|
||||
amiibo.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!_showAllAmiibo)
|
||||
{
|
||||
query = query.Where(amiibo =>
|
||||
amiibo.GamesSwitch.Any(game => game != null && game.GameId.Contains(TitleId)));
|
||||
}
|
||||
|
||||
query = _sortingAscending
|
||||
? query.OrderBy(amiibo => amiibo.Name)
|
||||
: query.OrderByDescending(amiibo => amiibo.Name);
|
||||
|
||||
_amiibos.AddRange(query);
|
||||
|
||||
int restoredIndex = -1;
|
||||
for (int i = 0; i < _amiibos.Count; i++)
|
||||
{
|
||||
if (_amiibos[i].GetId() == LastScannedAmiiboId)
|
||||
{
|
||||
if (!_showAllAmiibo)
|
||||
{
|
||||
foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
if (game.GameId.Contains(TitleId))
|
||||
{
|
||||
_amiibos.Add(amiiboSortedList[i]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_amiibos.Add(amiiboSortedList[i]);
|
||||
}
|
||||
restoredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AmiiboSelectedIndex = 0;
|
||||
AmiiboSelectedIndex = restoredIndex != -1
|
||||
? restoredIndex
|
||||
: (_amiibos.Count > 0 ? 0 : -1);
|
||||
|
||||
SetAmiiboDetails();
|
||||
}
|
||||
|
||||
private AmiiboSortField _sortField = AmiiboSortField.Name;
|
||||
|
||||
public AmiiboSortField SortingField
|
||||
{
|
||||
get => _sortField;
|
||||
set
|
||||
{
|
||||
_sortField = value;
|
||||
FilterAmiibo();
|
||||
OnPropertyChanged(nameof(SortingFieldName));
|
||||
OnPropertyChanged(nameof(IsSortedByName));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAmiiboDetails()
|
||||
{
|
||||
ResetAmiiboPreview();
|
||||
|
||||
Usage = string.Empty;
|
||||
|
||||
if (_amiiboSelectedIndex < 0 || _amiibos.Count < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
|
||||
|
||||
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
|
||||
string imageUrl = selected.Image;
|
||||
|
||||
StringBuilder usageStringBuilder = new();
|
||||
bool writable = false;
|
||||
|
||||
for (int i = 0; i < _amiiboList.Count; i++)
|
||||
foreach (AmiiboApiGamesSwitch game in selected.GamesSwitch)
|
||||
{
|
||||
if (_amiiboList[i].Equals(selected))
|
||||
if (game != null && game.GameId.Contains(TitleId))
|
||||
{
|
||||
bool writable = false;
|
||||
|
||||
foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
|
||||
foreach (AmiiboApiUsage usageItem in game.AmiiboUsage)
|
||||
{
|
||||
if (item.GameId.Contains(TitleId))
|
||||
{
|
||||
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||
{
|
||||
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
|
||||
|
||||
writable = usageItem.Write;
|
||||
}
|
||||
}
|
||||
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
|
||||
if (usageItem.Write)
|
||||
writable = true;
|
||||
}
|
||||
|
||||
if (usageStringBuilder.Length == 0)
|
||||
{
|
||||
usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}.");
|
||||
}
|
||||
|
||||
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : string.Empty)} : {usageStringBuilder}";
|
||||
}
|
||||
}
|
||||
|
||||
string usageLabel = writable
|
||||
? LocaleManager.Instance[LocaleKeys.Amiibo_WritableLabel]
|
||||
: LocaleManager.Instance[LocaleKeys.Amiibo_UsageLabel];
|
||||
|
||||
if (usageStringBuilder.Length == 0)
|
||||
{
|
||||
usageStringBuilder.Append(Environment.NewLine + Environment.NewLine + LocaleManager.Instance[LocaleKeys.Amiibo_UnknownLabel]);
|
||||
}
|
||||
else
|
||||
{
|
||||
usageStringBuilder.Replace(Environment.NewLine + "-", Environment.NewLine + Environment.NewLine + "-");
|
||||
}
|
||||
|
||||
Usage = usageLabel + usageStringBuilder.ToString();
|
||||
_ = UpdateAmiiboPreview(imageUrl);
|
||||
}
|
||||
|
||||
@@ -510,8 +562,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to request amiibo data: {exception}");
|
||||
}
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiFailFetchMessage],
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APITitle],
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APIFailFetchMessage],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
@@ -526,26 +578,33 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private async Task UpdateAmiiboPreview(string imageUrl)
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
|
||||
_imageCts.Cancel();
|
||||
_imageCts = new CancellationTokenSource();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
try
|
||||
{
|
||||
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
using MemoryStream memoryStream = new(amiiboPreviewBytes);
|
||||
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_imageCts.Token);
|
||||
|
||||
Bitmap bitmap = new(memoryStream);
|
||||
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl, linkedCts.Token);
|
||||
|
||||
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
|
||||
AmiiboImageSize / bitmap.Size.Height);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
byte[] bytes = await response.Content.ReadAsByteArrayAsync(linkedCts.Token);
|
||||
using MemoryStream ms = new(bytes);
|
||||
|
||||
int resizeHeight = (int)(bitmap.Size.Height * ratio);
|
||||
int resizeWidth = (int)(bitmap.Size.Width * ratio);
|
||||
Bitmap bitmap = new(ms);
|
||||
|
||||
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
|
||||
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width, AmiiboImageSize / bitmap.Size.Height);
|
||||
int newWidth = (int)(bitmap.Size.Width * ratio);
|
||||
int newHeight = (int)(bitmap.Size.Height * ratio);
|
||||
|
||||
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(newWidth, newHeight));
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}");
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to load amiibo preview: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,8 +619,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private static async Task ShowInfoDialog()
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage],
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APITitle],
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APIConnectErrorMessage],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -160,6 +160,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
[ObservableProperty] public partial bool UpdateAvailable { get; set; }
|
||||
|
||||
[ObservableProperty] public partial string SkylanderButtonHeader { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty] public partial bool SkylanderButtonEnabled { get; set; }
|
||||
|
||||
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
|
||||
{
|
||||
if (Updater.CanUpdate(true))
|
||||
@@ -204,6 +208,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
internal AppHost AppHost { get; set; }
|
||||
|
||||
public RelayCommand ToggleSkylanderCommand { get; }
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
Applications = [];
|
||||
@@ -233,6 +239,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Volume = ConfigurationState.Instance.System.AudioVolume;
|
||||
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
|
||||
}
|
||||
|
||||
ToggleSkylanderCommand = new RelayCommand(ExecuteToggleSkylander);
|
||||
}
|
||||
|
||||
~MainWindowViewModel()
|
||||
@@ -376,6 +384,50 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile;
|
||||
|
||||
[RelayCommand]
|
||||
private void UpdateSkylanderButton()
|
||||
{
|
||||
SkylanderButtonHeader = HasSkylander
|
||||
? LocaleManager.Instance[LocaleKeys.MenuBar_Actions_RemoveSkylanderButton]
|
||||
: LocaleManager.Instance[LocaleKeys.MenuBar_Actions_ScanSkylanderButton];
|
||||
|
||||
SkylanderButtonEnabled = HasSkylander || IsSkylanderRequested;
|
||||
|
||||
OnPropertyChanged(nameof(SkylanderButtonHeader));
|
||||
OnPropertyChanged(nameof(SkylanderButtonEnabled));
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(SkylanderButtonHeader));
|
||||
}, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
private void UpdateSkylanderState()
|
||||
{
|
||||
if (AppHost?.Device?.System == null) return;
|
||||
|
||||
IsSkylanderRequested = AppHost.Device.System.SearchingForSkylander(out _);
|
||||
HasSkylander = AppHost.Device.System.HasSkylander(out _);
|
||||
|
||||
UpdateSkylanderButton();
|
||||
}
|
||||
|
||||
private async void ExecuteToggleSkylander()
|
||||
{
|
||||
if (!IsGameRunning) return;
|
||||
|
||||
if (HasSkylander)
|
||||
{
|
||||
await RemoveSkylander();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ScanSkylander();
|
||||
}
|
||||
|
||||
UpdateSkylanderState();
|
||||
}
|
||||
|
||||
public bool IsSkylanderRequested
|
||||
{
|
||||
get => field && _isGameRunning;
|
||||
@@ -384,6 +436,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
UpdateSkylanderButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,17 +448,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSkylanderActions
|
||||
{
|
||||
get => field && _isGameRunning;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
UpdateSkylanderButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,6 +539,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
|
||||
|
||||
public bool IsXCIFile => Path.GetExtension(SelectedApplication.Path)?.ToLower() == ".xci";
|
||||
|
||||
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
|
||||
|
||||
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder &&
|
||||
@@ -790,7 +835,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
|
||||
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.GameListHeaderApplication],
|
||||
ApplicationSort.Title => LocaleManager.Instance[LocaleKeys.Common_Sort_NameLabel],
|
||||
ApplicationSort.Developer => LocaleManager.Instance[LocaleKeys.GameListSortDeveloper],
|
||||
ApplicationSort.LastPlayed => LocaleManager.Instance[LocaleKeys.GameListSortLastPlayed],
|
||||
ApplicationSort.TotalTimePlayed => LocaleManager.Instance[LocaleKeys.GameListSortTimePlayed],
|
||||
@@ -1258,7 +1303,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
break;
|
||||
case ShaderCacheLoadingState shaderCacheState:
|
||||
CacheLoadStatus = $"{current} / {total}";
|
||||
Splash = $"\"{SplashTextHelper.GetSplash()}\"";
|
||||
|
||||
string splash = SplashTextHelper.GetSplash();
|
||||
|
||||
if (!splash.IsNullOrEmpty())
|
||||
{
|
||||
Splash = $"\"{splash}\"";
|
||||
}
|
||||
switch (shaderCacheState)
|
||||
{
|
||||
case ShaderCacheLoadingState.Start:
|
||||
@@ -1488,7 +1539,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallFromFileFilePickerTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.xci", "*.zip"],
|
||||
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
|
||||
@@ -1672,7 +1723,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadApplicationFromFileFilePickerTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.nsp", "*.xci", "*.nca", "*.nro", "*.nso"],
|
||||
AppleUniformTypeIdentifiers =
|
||||
@@ -1759,7 +1810,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>?> PickFolders(LocaleKeys titleKey)
|
||||
private async Task<IReadOnlyList<string>> PickFolders(LocaleKeys titleKey)
|
||||
{
|
||||
return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
@@ -1935,13 +1986,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersion, version.VersionString);
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersionLabel, version.VersionString);
|
||||
|
||||
hasApplet = version.Major > 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersion, "NaN");
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersionLabel, "NaN");
|
||||
}
|
||||
|
||||
IsAppletMenuActive = hasApplet;
|
||||
@@ -2002,40 +2053,58 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OpenBinFile()
|
||||
public async Task ScanAmiiboFromBin()
|
||||
{
|
||||
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
|
||||
{
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.bin"],
|
||||
}
|
||||
}
|
||||
});
|
||||
bool shouldPause = ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value && IsGameRunning;
|
||||
|
||||
if (result.HasValue)
|
||||
if (shouldPause && AppHost?.Device?.System != null)
|
||||
{
|
||||
AppHost?.Pause();
|
||||
}
|
||||
try
|
||||
{
|
||||
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
|
||||
{
|
||||
AppHost.Device.System.ScanAmiiboFromBin(result.Value.Path.LocalPath);
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboFromBinFilePickerTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.bin"],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.HasValue)
|
||||
{
|
||||
AppHost.Device.System.ScanAmiiboFromBin(result.Value.Path.LocalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shouldPause && AppHost?.Device?.System != null)
|
||||
{
|
||||
AppHost?.Resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
public async Task OpenSkylanderWindow()
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ScanSkylander()
|
||||
{
|
||||
if (AppHost.Device.System.SearchingForSkylander(out int deviceId))
|
||||
{
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Skylanders_ScanSkylanderFilePickerTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"],
|
||||
},
|
||||
@@ -2058,12 +2127,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
AppHost.Device.System.ScanSkylander(deviceId, data);
|
||||
}
|
||||
}
|
||||
UpdateSkylanderButton();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RemoveSkylander()
|
||||
{
|
||||
AppHost.Device.System.RemoveSkylander();
|
||||
UpdateSkylanderButton();
|
||||
}
|
||||
|
||||
public void ReloadRenderDocApi()
|
||||
@@ -2153,7 +2225,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Window.Position.X + (int)(Window.Bounds.Width / 2),
|
||||
Window.Position.Y + (int)(Window.Bounds.Height / 2));
|
||||
|
||||
Avalonia.Platform.Screen? screen =
|
||||
Avalonia.Platform.Screen screen =
|
||||
Window.Screens.ScreenFromVisual(Window) ??
|
||||
Window.Screens.ScreenFromPoint(windowCenter) ??
|
||||
Window.Screens.Primary;
|
||||
@@ -2317,7 +2389,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
if (notifyUser != null)
|
||||
{
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText],
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_TrimFailedMessage],
|
||||
notifyUser
|
||||
);
|
||||
}
|
||||
@@ -2343,18 +2415,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (trimmer.CanBeTrimmed)
|
||||
{
|
||||
double savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0;
|
||||
double currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0;
|
||||
double cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0;
|
||||
int savings = (int)Math.Round((double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0);
|
||||
int currentFileSize = (int)Math.Round((double)trimmer.FileSizeB / 1024.0 / 1024.0);
|
||||
int cartDataSize = (int)Math.Round((double)trimmer.DataSizeB / 1024.0 / 1024.0);
|
||||
string secondaryText = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.TrimXCIFileDialogSecondaryText, currentFileSize, cartDataSize, savings);
|
||||
LocaleKeys.Dialog_XCITrimmer_SecondaryMessage, currentFileSize.ToString("0"), cartDataSize.ToString("0"), savings.ToString("0"));
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogPrimaryText],
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_XCITrimmer_PrimaryMessage],
|
||||
secondaryText,
|
||||
LocaleManager.Instance[LocaleKeys.Continue],
|
||||
LocaleManager.Instance[LocaleKeys.Cancel],
|
||||
LocaleManager.Instance[LocaleKeys.GameListContextMenuTrimXCI]
|
||||
LocaleManager.Instance[LocaleKeys.GameListContextMenu_TrimXCIButton]
|
||||
);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
@@ -2364,8 +2436,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusBarProgressStatusText =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileTrimming,
|
||||
Path.GetFileName(filename));
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_TrimmingXCILabel,
|
||||
Path.GetFileNameWithoutExtension(filename));
|
||||
StatusBarProgressStatusVisible = true;
|
||||
StatusBarProgressMaximum = 1;
|
||||
StatusBarProgressValue = 0;
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
AllowMultiple = true,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.nsp"],
|
||||
AppleUniformTypeIdentifiers = ["com.ryujinx.nsp"],
|
||||
|
||||
+101
-29
@@ -29,11 +29,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public enum SortField
|
||||
{
|
||||
Name,
|
||||
Saved
|
||||
Savings,
|
||||
Status
|
||||
}
|
||||
|
||||
private const string _FileExtXCI = "XCI";
|
||||
|
||||
private const string _fileExtXCI = "XCI";
|
||||
private readonly Ryujinx.Common.Logging.XCIFileTrimmerLog _logger;
|
||||
private ApplicationLibrary ApplicationLibrary => _mainWindowViewModel.ApplicationLibrary;
|
||||
private Optional<XCITrimmerFileModel> _processingApplication = null;
|
||||
@@ -45,6 +45,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private string _search;
|
||||
private ProcessingMode _processingMode;
|
||||
private SortField _sortField = SortField.Name;
|
||||
private int _processingCurrent;
|
||||
private int _processingTotal;
|
||||
|
||||
|
||||
public XciTrimmerViewModel(MainWindowViewModel mainWindowViewModel)
|
||||
{
|
||||
@@ -56,7 +59,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private void LoadXCIApplications()
|
||||
{
|
||||
IEnumerable<ApplicationData> apps = ApplicationLibrary.Applications.Items
|
||||
.Where(app => app.FileExtension == _FileExtXCI);
|
||||
.Where(app => app.FileExtension == _fileExtXCI);
|
||||
|
||||
foreach (ApplicationData xciApp in apps)
|
||||
AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path));
|
||||
@@ -64,11 +67,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
ApplicationsChanged();
|
||||
}
|
||||
|
||||
private XCITrimmerFileModel CreateXCITrimmerFile(
|
||||
string path,
|
||||
OperationOutcome operationOutcome = OperationOutcome.Undetermined)
|
||||
private XCITrimmerFileModel CreateXCITrimmerFile(string path, OperationOutcome operationOutcome = OperationOutcome.Undetermined)
|
||||
{
|
||||
ApplicationData xciApp = ApplicationLibrary.Applications.Items.First(app => app.FileExtension == _FileExtXCI && app.Path == path);
|
||||
ApplicationData xciApp = ApplicationLibrary.Applications.Items.First(app => app.FileExtension == _fileExtXCI && app.Path == path);
|
||||
return XCITrimmerFileModel.FromApplicationData(xciApp, _logger) with { ProcessingOutcome = operationOutcome };
|
||||
}
|
||||
|
||||
@@ -90,11 +91,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
SortAndFilter();
|
||||
}
|
||||
|
||||
public bool AnySelected =>
|
||||
_selectedXCIFiles.Count > 0;
|
||||
|
||||
private void SortingChanged()
|
||||
{
|
||||
OnPropertiesChanged(
|
||||
nameof(IsSortedByName),
|
||||
nameof(IsSortedBySaved),
|
||||
nameof(IsSortedBySavings),
|
||||
nameof(IsSortedByStatus),
|
||||
nameof(SortingAscending),
|
||||
nameof(SortingField),
|
||||
nameof(SortingFieldName));
|
||||
@@ -114,6 +119,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
nameof(Status),
|
||||
nameof(PotentialSavings),
|
||||
nameof(ActualSavings),
|
||||
nameof(SavingsDifference),
|
||||
nameof(CanTrim),
|
||||
nameof(CanUntrim));
|
||||
|
||||
@@ -123,16 +129,30 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void SelectionChanged(bool displayedChanged = true)
|
||||
{
|
||||
OnPropertiesChanged(
|
||||
nameof(Status),
|
||||
nameof(CanTrim),
|
||||
nameof(CanUntrim),
|
||||
nameof(SelectedXCIFiles));
|
||||
OnPropertyChanged(nameof(Status));
|
||||
OnPropertyChanged(nameof(CanTrim));
|
||||
OnPropertyChanged(nameof(CanUntrim));
|
||||
OnPropertyChanged(nameof(SelectedXCIFiles));
|
||||
OnPropertyChanged(nameof(AnySelected));
|
||||
OnPropertyChanged(nameof(SelectToggleText));
|
||||
|
||||
if (displayedChanged)
|
||||
OnPropertyChanged(nameof(SelectedDisplayedXCIFiles));
|
||||
}
|
||||
|
||||
public void ToggleSelect()
|
||||
{
|
||||
if (AnySelected)
|
||||
DeselectAll();
|
||||
else
|
||||
SelectAll();
|
||||
}
|
||||
|
||||
public string SelectToggleText =>
|
||||
AnySelected
|
||||
? LocaleManager.Instance[LocaleKeys.XCITrimmer_ClearSelectionButton]
|
||||
: LocaleManager.Instance[LocaleKeys.XCITrimmer_SelectAllButton];
|
||||
|
||||
private void ProcessingChanged()
|
||||
{
|
||||
OnPropertiesChanged(
|
||||
@@ -167,6 +187,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
(processingMode == ProcessingMode.Trimming && xci.Trimmable)
|
||||
)).ToList();
|
||||
|
||||
_processingTotal = toProcess.Count;
|
||||
_processingCurrent = 0;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(Status));
|
||||
});
|
||||
|
||||
List<XCITrimmerFileModel> viewsSaved = DisplayedXCIFiles.ToList();
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
@@ -219,6 +247,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
ProcessingApplication = null;
|
||||
});
|
||||
}
|
||||
_processingCurrent++;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(Status));
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -226,9 +260,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_displayedXCIFiles.AddOrReplaceMatching(_allXCIFiles, viewsSaved);
|
||||
_selectedXCIFiles.AddOrReplaceMatching(_allXCIFiles, toProcess);
|
||||
|
||||
Processing = false;
|
||||
ApplicationsChanged();
|
||||
|
||||
_selectedXCIFiles.Clear();
|
||||
|
||||
foreach (var processed in toProcess)
|
||||
{
|
||||
var updated = _allXCIFiles.FirstOrDefault(x => x.Path == processed.Path);
|
||||
if (updated != null)
|
||||
_selectedXCIFiles.Add(updated);
|
||||
}
|
||||
|
||||
SelectionChanged();
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -270,9 +315,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
case SortField.Name:
|
||||
result = x.Name.CompareTo(y.Name);
|
||||
break;
|
||||
case SortField.Saved:
|
||||
case SortField.Savings:
|
||||
result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB);
|
||||
break;
|
||||
case SortField.Status:
|
||||
|
||||
result = x.CurrentSavingsB.CompareTo(y.CurrentSavingsB);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_viewModel.SortingAscending)
|
||||
@@ -312,15 +361,16 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectDisplayed()
|
||||
public void SelectAll()
|
||||
{
|
||||
SelectedXCIFiles.Clear();
|
||||
SelectedXCIFiles.AddRange(DisplayedXCIFiles);
|
||||
SelectionChanged();
|
||||
}
|
||||
|
||||
public void DeselectDisplayed()
|
||||
public void DeselectAll()
|
||||
{
|
||||
SelectedXCIFiles.RemoveMany(DisplayedXCIFiles);
|
||||
SelectedXCIFiles.Clear();
|
||||
SelectionChanged();
|
||||
}
|
||||
|
||||
@@ -426,16 +476,23 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
return _processingMode switch
|
||||
{
|
||||
ProcessingMode.Trimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusTrimming], DisplayedXCIFiles.Count),
|
||||
ProcessingMode.Untrimming => string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusUntrimming], DisplayedXCIFiles.Count),
|
||||
ProcessingMode.Trimming => string.Format(
|
||||
LocaleManager.Instance[LocaleKeys.XCITrimmer_StatusTrimmingLabel],
|
||||
_processingCurrent,
|
||||
_processingTotal),
|
||||
|
||||
ProcessingMode.Untrimming => string.Format(
|
||||
LocaleManager.Instance[LocaleKeys.XCITrimmer_StatusUntrimmingLabel],
|
||||
_processingCurrent,
|
||||
_processingTotal),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.IsNullOrEmpty(Search) ?
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCount], SelectedXCIFiles.Count, AllXCIFiles.Count) :
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerTitleStatusCountWithFilter], SelectedXCIFiles.Count, AllXCIFiles.Count, DisplayedXCIFiles.Count);
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmer_StatusCountLabel], SelectedXCIFiles.Count, AllXCIFiles.Count) :
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmer_StatusCountWithFilterLabel], SelectedXCIFiles.Count, AllXCIFiles.Count, DisplayedXCIFiles.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,8 +523,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
return SortingField switch
|
||||
{
|
||||
SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName],
|
||||
SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved],
|
||||
SortField.Name => LocaleManager.Instance[LocaleKeys.Common_Sort_NameLabel],
|
||||
SortField.Savings => LocaleManager.Instance[LocaleKeys.Common_Sort_SavingsLabel],
|
||||
SortField.Status => LocaleManager.Instance[LocaleKeys.Common_Sort_TrimStatusLabel],
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
@@ -488,11 +546,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
get => _sortField == SortField.Name;
|
||||
}
|
||||
|
||||
public bool IsSortedBySaved
|
||||
public bool IsSortedBySavings
|
||||
{
|
||||
get => _sortField == SortField.Saved;
|
||||
get => _sortField == SortField.Savings;
|
||||
}
|
||||
|
||||
public bool IsSortedByStatus => _sortField == SortField.Status;
|
||||
|
||||
public AvaloniaList<XCITrimmerFileModel> SelectedXCIFiles
|
||||
{
|
||||
get => _selectedXCIFiles;
|
||||
@@ -517,7 +577,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.PotentialSavingsB / BytesPerMb));
|
||||
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmer_MBLabel], AllXCIFiles.Sum(xci => xci.PotentialSavingsB / BytesPerMb));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +585,19 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerSavingsMb], AllXCIFiles.Sum(xci => xci.CurrentSavingsB / BytesPerMb));
|
||||
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmer_MBLabel], AllXCIFiles.Sum(xci => xci.CurrentSavingsB / BytesPerMb));
|
||||
}
|
||||
}
|
||||
|
||||
public string SavingsDifference
|
||||
{
|
||||
get
|
||||
{
|
||||
long potentialSavings = AllXCIFiles.Sum(xci => xci.PotentialSavingsB);
|
||||
long actualSavings = AllXCIFiles.Sum(xci => xci.CurrentSavingsB);
|
||||
long differenceMb = (potentialSavings - actualSavings) / BytesPerMb;
|
||||
|
||||
return string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmer_MBLabel], differenceMb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
MinHeight="29"
|
||||
MaxHeight="29"
|
||||
HorizontalAlignment="Stretch"
|
||||
Watermark="{ext:Locale Search}"
|
||||
Watermark="{ext:Locale Common_Search_SearchWatermark}"
|
||||
Text="{Binding Search}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
@@ -190,7 +190,7 @@
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Click="Close">
|
||||
<TextBlock Text="{ext:Locale InputDialogCancel}" />
|
||||
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
MinHeight="27"
|
||||
MaxHeight="27"
|
||||
HorizontalAlignment="Stretch"
|
||||
Watermark="{ext:Locale Search}"
|
||||
Watermark="{ext:Locale Common_Search_SearchWatermark}"
|
||||
Text="{Binding Search}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
@@ -158,7 +158,7 @@
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Click="Close">
|
||||
<TextBlock Text="{ext:Locale InputDialogCancel}" />
|
||||
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Click="Close">
|
||||
<TextBlock Text="{ext:Locale InputDialogCancel}" />
|
||||
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Dialog.XciTrimmerView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.Common.Models"
|
||||
Width="700"
|
||||
Height="600"
|
||||
x:DataType="viewModels:XciTrimmerViewModel"
|
||||
Focusable="True"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="25,10,25,0" RowDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
<Panel Margin="0,0,0,10" Grid.Row="0">
|
||||
<TextBlock Text="{Binding Status}" />
|
||||
</Panel>
|
||||
<Grid Margin="0,0,0,10" Grid.Row="1" IsVisible="{Binding !Processing}" ColumnDefinitions="Auto,*">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Button Margin="0,0,10,0" MinWidth="90" Click="ToggleSelect">
|
||||
<TextBlock Text="{Binding SelectToggleText}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right">
|
||||
<DropDownButton Width="170" HorizontalAlignment="Right" VerticalAlignment="Center" Content="{Binding SortingFieldName}">
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<StackPanel Margin="0" HorizontalAlignment="Stretch" Orientation="Vertical">
|
||||
<StackPanel>
|
||||
<RadioButton Checked="Sort_Checked" Content="{ext:Locale Common_Sort_NameLabel}" GroupName="Sort" IsChecked="{Binding IsSortedByName, Mode=OneTime}" Tag="Name" />
|
||||
<RadioButton Checked="Sort_Checked" Content="{ext:Locale Common_Sort_SavingsLabel}" GroupName="Sort" IsChecked="{Binding IsSortedBySavings, Mode=OneTime}" Tag="Savings" />
|
||||
<RadioButton Checked="Sort_Checked" Content="{ext:Locale Common_Sort_TrimStatusLabel}" GroupName="Sort" IsChecked="{Binding IsSortedByStatus, Mode=OneTime}" Tag="Status" />
|
||||
</StackPanel>
|
||||
<Border Width="60" Height="2" Margin="5" HorizontalAlignment="Stretch" BorderBrush="White" BorderThickness="0,1,0,0">
|
||||
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
<RadioButton Checked="Order_Checked" Content="{ext:Locale Common_Sort_OrderAscending}" GroupName="Order" IsChecked="{Binding SortingAscending, Mode=OneTime}" Tag="Ascending" />
|
||||
<RadioButton Checked="Order_Checked" Content="{ext:Locale Common_Sort_OrderDescending}" GroupName="Order" IsChecked="{Binding !SortingAscending, Mode=OneTime}" Tag="Descending" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
<TextBox
|
||||
Width="200"
|
||||
MaxWidth="200"
|
||||
Margin="5,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Watermark="{ext:Locale Common_Search_SearchWatermark}"
|
||||
Text="{Binding Search}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="0,0,0,20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Padding="2.5">
|
||||
<ListBox
|
||||
AutoScrollToSelectedItem="{Binding Processing}"
|
||||
SelectedItem="{Binding NullableProcessingApplication}"
|
||||
SelectionMode="Multiple, Toggle"
|
||||
Background="Transparent"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
SelectedItems="{Binding SelectedDisplayedXCIFiles, Mode=OneWay}"
|
||||
ItemsSource="{Binding DisplayedXCIFiles}"
|
||||
IsEnabled="{Binding !Processing}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate DataType="models:XCITrimmerFileModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<ui:SymbolIcon
|
||||
Grid.Column="0"
|
||||
Margin="10,0,20,0"
|
||||
Width="15"
|
||||
Height="15"
|
||||
FontSize="15"
|
||||
FlowDirection="LeftToRight"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Symbol="{Binding Converter={x:Static helpers:XCITrimmerFileStatusConverter.Instance}}">
|
||||
<ToolTip.Tip>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Text="{Binding StatusText}" />
|
||||
<TextBlock
|
||||
MaxLines="5"
|
||||
MaxWidth="200"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasStatusDetail}"
|
||||
Text="{Binding ., Converter={x:Static helpers:XCITrimmerFileStatusDetailConverter.Instance}}" />
|
||||
</StackPanel>
|
||||
</ToolTip.Tip>
|
||||
</ui:SymbolIcon>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding Name}" />
|
||||
<Grid
|
||||
Grid.Column="2"
|
||||
MinWidth="120"
|
||||
ColumnDefinitions="*">
|
||||
<ProgressBar
|
||||
Height="10"
|
||||
Margin="10,0,10,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding !$parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Value="{Binding PercentageProgress}" />
|
||||
<TextBlock
|
||||
Margin="10,0,10,0"
|
||||
FlowDirection="LeftToRight"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="1"
|
||||
IsVisible="{Binding !$parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing}"
|
||||
Text="{Binding ., Converter={x:Static helpers:XCITrimmerFileSpaceSavingsConverter.Instance}}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<StackPanel Grid.Row="3" Margin="0,0,0,20" Spacing="5" FlowDirection="LeftToRight" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock Classes="h1" HorizontalAlignment="Center" VerticalAlignment="Center" MaxLines="1" Text="{ext:Locale XCITrimmer_SavedLabel}" />
|
||||
<TextBlock Classes="h1" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Regular" MaxLines="1" Text="{Binding ActualSavings}" />
|
||||
<TextBlock Classes="h1" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Regular" MaxLines="1" Text="•" />
|
||||
<TextBlock Classes="h1" HorizontalAlignment="Center" VerticalAlignment="Center" MaxLines="1" Text="{ext:Locale XCITrimmer_RemainingLabel}" />
|
||||
<TextBlock Classes="h1" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Regular" MaxLines="1" Text="{Binding SavingsDifference}" />
|
||||
</StackPanel>
|
||||
<Panel Grid.Row="4" Margin="0,10,0,0" HorizontalAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Left">
|
||||
<Button Name="TrimButton" MinWidth="90" Click="Trim" IsEnabled="{Binding CanTrim}">
|
||||
<TextBlock Text="{ext:Locale XCITrimmer_TrimButton}" />
|
||||
</Button>
|
||||
<Button Name="UntrimButton" MinWidth="90" Click="Untrim" IsEnabled="{Binding CanUntrim}">
|
||||
<TextBlock Text="{ext:Locale XCITrimmer_UntrimButton}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||
<Button Name="CancellingButton" MinWidth="90" Click="Cancel" IsEnabled="False">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="Processing" />
|
||||
<Binding Path="Cancel" />
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<TextBlock Text="{ext:Locale InputDialogCancelling}" />
|
||||
</Button>
|
||||
<Button Name="CancelButton" MinWidth="90" Click="Cancel">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="Processing" />
|
||||
<Binding Path="!Cancel" />
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
|
||||
</Button>
|
||||
<Button Name="CloseButton" MinWidth="90" Click="Close" IsVisible="{Binding !Processing}">
|
||||
<TextBlock Text="{ext:Locale SettingsButtonClose}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
+8
-2
@@ -18,6 +18,12 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ToggleSelect(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is XciTrimmerViewModel vm)
|
||||
vm.ToggleSelect();
|
||||
}
|
||||
|
||||
public static async Task Show()
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
@@ -29,7 +35,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
{
|
||||
ViewModel = new XciTrimmerViewModel(RyujinxApp.MainWindow.ViewModel)
|
||||
},
|
||||
Title = LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]
|
||||
Title = LocaleManager.Instance[LocaleKeys.MenuBar_Actions_XCITrimmerButton]
|
||||
};
|
||||
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
@@ -52,7 +58,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
|
||||
private void Close(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((ContentDialog)Parent).Hide();
|
||||
((ContentDialog)Parent!).Hide();
|
||||
}
|
||||
|
||||
private void Cancel(Object sender, RoutedEventArgs e)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user